<?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>Report — 開發過程的事後檢討 on Tarragon</title><link>https://tarrragon.github.io/blog/report/</link><description>Recent content in Report — 開發過程的事後檢討 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/report/index.xml" rel="self" type="application/rss+xml"/><item><title>在外部組件上加客製功能：以邊界為中心的方法選擇</title><link>https://tarrragon.github.io/blog/report/external-component-customization/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/external-component-customization/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>客製外部組件的穩定性與「離組件邊界多近」成正比。&lt;/strong> 組件作者維護組件內部的一致性；客製貼著組件的對外介面（CLI 參數、CSS reset 邊界、CSS layer）做，組件升級不會打到客製。深入組件內部（注入子節點、改 source 行為）的客製則仰賴組件的內部實作，作者不保證下個版本還相容。&lt;/p>
&lt;p>這條原則對應到 Pagefind 整合時的三個具體情境，本文逐一拆解。&lt;/p>
&lt;hr>
&lt;h2 id="邊界概念為什麼用邊界看外部組件">邊界概念：為什麼用「邊界」看外部組件&lt;/h2>
&lt;h3 id="這類問題的本質商業邏輯">這類問題的本質（商業邏輯）&lt;/h3>
&lt;p>組件 = 一組對外契約 + 內部實作。對外契約是作者保證的東西（CLI 參數、CSS class name、CSS variable hooks），內部實作是達成契約的手段、可能在版本之間變動。&lt;/p>
&lt;p>客製貼著對外契約做，等於跟作者站在同一邊；客製動到內部實作，等於跟作者每個版本對抗。&lt;/p>
&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;/td>
 &lt;td>介面契約（穩定）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件 DOM 邊界外&lt;/td>
 &lt;td>邊界元素的 class / id 名稱（半穩定）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件 DOM 邊界內&lt;/td>
 &lt;td>內部 DOM 結構（隨框架渲染週期變動）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件內部邏輯&lt;/td>
 &lt;td>source code（升級時必衝突）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="這次任務涉及的邊界case">這次任務涉及的邊界（CASE）&lt;/h3>
&lt;p>Pagefind 提供三條值得辨識的邊界：&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;code>--root-selector&lt;/code> 參數&lt;/td>
 &lt;td>「組件看到什麼資料」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重置邊界&lt;/td>
 &lt;td>&lt;code>.pagefind-ui--reset&lt;/code> class&lt;/td>
 &lt;td>對 UA 樣式的覆寫範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>specificity 邊界&lt;/td>
 &lt;td>svelte hash class（雙寫到 30）&lt;/td>
 &lt;td>組件 CSS 的權重起點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每條邊界各有對應的客製介面 — 緊貼這些介面比繞道進內部安全得多。&lt;/p>
&lt;hr>
&lt;h2 id="索引邊界用-root-selector-限縮搜尋範圍">索引邊界：用 root-selector 限縮搜尋範圍&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：Pagefind 的索引流程透過 &lt;code>--root-selector&lt;/code> 參數控制「哪些 HTML 進索引」。這是組件預期的客製介面。&lt;/p>
&lt;h3 id="這次的觀察">這次的觀察&lt;/h3>
&lt;p>theme 的 site header 包含 &lt;code>&amp;lt;h1&amp;gt;{{ .Site.Title }}&amp;lt;/h1&amp;gt;&lt;/code>，每一頁 DOM 上的第一個 h1 都是站名「Tarragon」。Pagefind 預設取頁面第一個 h1 當搜尋結果 title — 結果所有結果都顯示「Tarragon」。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>第一個 h1 是站名而非文章名 — 索引「看到」的範圍跟我們以為的不同。要修的不是 theme（影響其他頁面），是 Pagefind 的 input 範圍。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">npx -y pagefind --site public --root-selector main&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>&amp;lt;main&amp;gt;&lt;/code> 內才包含文章 h1；site header 在 &lt;code>&amp;lt;body&amp;gt;&lt;/code> 直接子節點、不在 &lt;code>&amp;lt;main&amp;gt;&lt;/code> 內，自然被排除。&lt;/p>
&lt;p>&lt;strong>完成標準&lt;/strong>：每一筆結果的 title 顯示文章 h1，而非站名。&lt;/p>
&lt;hr>
&lt;h2 id="重置邊界在落腳處重建-ua-樣式">重置邊界：在落腳處重建 UA 樣式&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：CSS reset 的有效範圍與 ancestor class 綁定。元素換位置就換 reset context — 這是 CSS 的基本機制，不是 bug。&lt;/p>
&lt;h3 id="這次的觀察-1">這次的觀察&lt;/h3>
&lt;p>Filter UI（&lt;code>.pagefind-ui__filter-panel&lt;/code>，本質是 &lt;code>&amp;lt;fieldset&amp;gt;&lt;/code>）需要從 &lt;code>.pagefind-ui&lt;/code> 搬到外部 aside（左側 sidebar）。搬完後 fieldset 的瀏覽器預設邊框冒出來。&lt;/p>
&lt;h3 id="判讀-1">判讀&lt;/h3>
&lt;p>&lt;code>.pagefind-ui--reset&lt;/code> 用 &lt;code>all: unset&lt;/code> 重置所有後代元素的 UA 樣式。fieldset 搬出 &lt;code>.pagefind-ui&lt;/code> 後，這個 ancestor 不在了，UA 樣式（包括 fieldset 預設邊框）回到原樣。&lt;/p>
&lt;h3 id="執行-1">執行&lt;/h3>
&lt;p>在 fieldset 落腳處（aside 內）重新關掉 UA 樣式：&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="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="nt">fieldset&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&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="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&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="k">margin&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>完成標準&lt;/strong>：fieldset 在新位置看起來跟在 &lt;code>.pagefind-ui&lt;/code> 內一樣 — 沒有多餘邊框。&lt;/p>
&lt;hr>
&lt;h2 id="specificity-邊界跳出線性比較戰場">specificity 邊界：跳出線性比較戰場&lt;/h2>
&lt;p>&lt;strong>核心定義&lt;/strong>：當組件透過 hash class 把 specificity 拉高到一般客製寫法蓋不過時，邊界落在 CSS 樣式分層機制 — 不在個別 selector 數字。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>客製外部組件的穩定性與「離組件邊界多近」成正比。</strong> 組件作者維護組件內部的一致性；客製貼著組件的對外介面（CLI 參數、CSS reset 邊界、CSS layer）做，組件升級不會打到客製。深入組件內部（注入子節點、改 source 行為）的客製則仰賴組件的內部實作，作者不保證下個版本還相容。</p>
<p>這條原則對應到 Pagefind 整合時的三個具體情境，本文逐一拆解。</p>
<hr>
<h2 id="邊界概念為什麼用邊界看外部組件">邊界概念：為什麼用「邊界」看外部組件</h2>
<h3 id="這類問題的本質商業邏輯">這類問題的本質（商業邏輯）</h3>
<p>組件 = 一組對外契約 + 內部實作。對外契約是作者保證的東西（CLI 參數、CSS class name、CSS variable hooks），內部實作是達成契約的手段、可能在版本之間變動。</p>
<p>客製貼著對外契約做，等於跟作者站在同一邊；客製動到內部實作，等於跟作者每個版本對抗。</p>
<p>「邊界」這個概念把組件分成「契約面」與「實作面」 — 客製選位置時，依離邊界遠近排序：</p>
<table>
  <thead>
      <tr>
          <th>客製位置</th>
          <th>跨越時的依賴</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件對外介面</td>
          <td>介面契約（穩定）</td>
      </tr>
      <tr>
          <td>組件 DOM 邊界外</td>
          <td>邊界元素的 class / id 名稱（半穩定）</td>
      </tr>
      <tr>
          <td>組件 DOM 邊界內</td>
          <td>內部 DOM 結構（隨框架渲染週期變動）</td>
      </tr>
      <tr>
          <td>組件內部邏輯</td>
          <td>source code（升級時必衝突）</td>
      </tr>
  </tbody>
</table>
<h3 id="這次任務涉及的邊界case">這次任務涉及的邊界（CASE）</h3>
<p>Pagefind 提供三條值得辨識的邊界：</p>
<table>
  <thead>
      <tr>
          <th>邊界名稱</th>
          <th>介面形式</th>
          <th>範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引邊界</td>
          <td><code>--root-selector</code> 參數</td>
          <td>「組件看到什麼資料」</td>
      </tr>
      <tr>
          <td>重置邊界</td>
          <td><code>.pagefind-ui--reset</code> class</td>
          <td>對 UA 樣式的覆寫範圍</td>
      </tr>
      <tr>
          <td>specificity 邊界</td>
          <td>svelte hash class（雙寫到 30）</td>
          <td>組件 CSS 的權重起點</td>
      </tr>
  </tbody>
</table>
<p>每條邊界各有對應的客製介面 — 緊貼這些介面比繞道進內部安全得多。</p>
<hr>
<h2 id="索引邊界用-root-selector-限縮搜尋範圍">索引邊界：用 root-selector 限縮搜尋範圍</h2>
<p><strong>核心定義</strong>：Pagefind 的索引流程透過 <code>--root-selector</code> 參數控制「哪些 HTML 進索引」。這是組件預期的客製介面。</p>
<h3 id="這次的觀察">這次的觀察</h3>
<p>theme 的 site header 包含 <code>&lt;h1&gt;{{ .Site.Title }}&lt;/h1&gt;</code>，每一頁 DOM 上的第一個 h1 都是站名「Tarragon」。Pagefind 預設取頁面第一個 h1 當搜尋結果 title — 結果所有結果都顯示「Tarragon」。</p>
<h3 id="判讀">判讀</h3>
<p>第一個 h1 是站名而非文章名 — 索引「看到」的範圍跟我們以為的不同。要修的不是 theme（影響其他頁面），是 Pagefind 的 input 範圍。</p>
<h3 id="執行">執行</h3>





<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">npx -y pagefind --site public --root-selector main</span></span></code></pre></div><p><code>&lt;main&gt;</code> 內才包含文章 h1；site header 在 <code>&lt;body&gt;</code> 直接子節點、不在 <code>&lt;main&gt;</code> 內，自然被排除。</p>
<p><strong>完成標準</strong>：每一筆結果的 title 顯示文章 h1，而非站名。</p>
<hr>
<h2 id="重置邊界在落腳處重建-ua-樣式">重置邊界：在落腳處重建 UA 樣式</h2>
<p><strong>核心定義</strong>：CSS reset 的有效範圍與 ancestor class 綁定。元素換位置就換 reset context — 這是 CSS 的基本機制，不是 bug。</p>
<h3 id="這次的觀察-1">這次的觀察</h3>
<p>Filter UI（<code>.pagefind-ui__filter-panel</code>，本質是 <code>&lt;fieldset&gt;</code>）需要從 <code>.pagefind-ui</code> 搬到外部 aside（左側 sidebar）。搬完後 fieldset 的瀏覽器預設邊框冒出來。</p>
<h3 id="判讀-1">判讀</h3>
<p><code>.pagefind-ui--reset</code> 用 <code>all: unset</code> 重置所有後代元素的 UA 樣式。fieldset 搬出 <code>.pagefind-ui</code> 後，這個 ancestor 不在了，UA 樣式（包括 fieldset 預設邊框）回到原樣。</p>
<h3 id="執行-1">執行</h3>
<p>在 fieldset 落腳處（aside 內）重新關掉 UA 樣式：</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="p">.</span><span class="nc">search-filter-slot</span> <span class="nt">fieldset</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>完成標準</strong>：fieldset 在新位置看起來跟在 <code>.pagefind-ui</code> 內一樣 — 沒有多餘邊框。</p>
<hr>
<h2 id="specificity-邊界跳出線性比較戰場">specificity 邊界：跳出線性比較戰場</h2>
<p><strong>核心定義</strong>：當組件透過 hash class 把 specificity 拉高到一般客製寫法蓋不過時，邊界落在 CSS 樣式分層機制 — 不在個別 selector 數字。</p>
<h3 id="這次的觀察-2">這次的觀察</h3>
<p>Pagefind 透過 svelte 把 class name 加 hash 重複寫進 selector（<code>.x.svelte-y.svelte-y</code>），把 specificity 從 10 提升到 30。一般客製 CSS specificity 10-20、蓋不過去。</p>
<h3 id="判讀-2">判讀</h3>
<p>Specificity 是線性數字比較 — 跟組件作者比 specificity 是無贏的軍備競賽。要真正解這類覆寫戰、需要跳出「線性比較」這個維度本身。</p>
<h3 id="執行-2">執行</h3>
<p>具體做法（<code>@import url(...) layer(...)</code>）與升級兼容性、其他外部組件的 layer 策略，由 <a href="../css-layers-over-specificity/">#24 CSS Layers 取代 specificity 戰</a> 完整展開。在邊界辨識上、本篇要記住的是：<strong>遇到 specificity 30+ 的覆寫戰、不要往 <code>!important</code> / <code>.x.x</code> 雙寫的方向加碼、改去看 layer 維度</strong>。</p>
<p><strong>完成標準</strong>：所有原本需要 <code>!important</code> 或 <code>.x.x</code> 雙寫的覆寫，可以用單純的 class selector 寫。</p>
<hr>
<h2 id="客製深度的內在屬性比較">客製深度的內在屬性比較</h2>
<p>選擇客製位置時，用三個<strong>內在屬性</strong>比較 — 不用「實作要多久」這類時間維度：</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>依賴前提</th>
          <th>升級風險</th>
          <th>可逆性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件對外介面</td>
          <td>介面 stable across 版本</td>
          <td>低 — 介面是公開契約</td>
          <td>高 — 改一個參數即可還原</td>
      </tr>
      <tr>
          <td>DOM 邊界外（class hooks）</td>
          <td>邊界元素 class / id 穩定</td>
          <td>中 — class 改名才打破</td>
          <td>中 — 改選擇器即可</td>
      </tr>
      <tr>
          <td>DOM 邊界內</td>
          <td>內部 DOM 結構穩定</td>
          <td>高 — 框架重繪可能即時打破</td>
          <td>低 — 客製跟內部結構深度耦合</td>
      </tr>
      <tr>
          <td>內部邏輯（fork / patch）</td>
          <td>整個組件 source</td>
          <td>最高 — 升級必衝突</td>
          <td>最低 — 重新 merge</td>
      </tr>
  </tbody>
</table>
<p><strong>選擇順序</strong>：先試最外層；不夠用再往內推一層。每往內一層、依賴前提增加、升級風險上升、可逆性下降。</p>
<hr>
<h2 id="設計取捨客製位置的層次選擇">設計取捨：客製位置的層次選擇</h2>
<p>四種做法、各自機會成本不同。預設選最外層（A）、不夠用才往內推一層。</p>
<blockquote>
<p>本篇是 <a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a> 抽象原則在「客製位置選擇」這個面向的應用。</p></blockquote>
<h3 id="a組件公開介面層最佳">A：組件公開介面層（最佳）</h3>
<ul>
<li><strong>機制</strong>：用組件提供的 CLI 參數、props、CSS variable hook、event API 客製</li>
<li><strong>選 A 的理由</strong>：作者保證跨版本相容、客製跟組件升級無關</li>
<li><strong>適合</strong>：能被介面覆蓋的客製需求（多數常見場景）</li>
<li><strong>代價</strong>：受介面範圍限制、超出範圍只能往內推</li>
</ul>
<h3 id="b鄰接層class-hooksreset-邊界這個專案的次選">B：鄰接層（class hooks、reset 邊界）（這個專案的次選）</h3>
<ul>
<li><strong>機制</strong>：用組件邊界元素的 class / id / data attribute 寫客製 CSS、注意 CSS reset 邊界</li>
<li><strong>跟 A 的取捨</strong>：B 介面外但仍貼著組件邊界、A 在介面內；B 在 class 名穩定時 OK、改名就壞</li>
<li><strong>B 比 A 好的情境</strong>：組件未提供對應介面、但有穩定的 class 邊界可掛勾</li>
</ul>
<h3 id="c邊界內-dom-操作">C：邊界內 DOM 操作</h3>
<ul>
<li><strong>機制</strong>：JS 改組件內部 DOM 結構 / 節點屬性 / 注入元素</li>
<li><strong>跟 A/B 的取捨</strong>：C 直接操控、跟 framework reconciliation 競爭；風險顯著上升</li>
<li><strong>C 才合理的情境</strong>：A/B 都無法達成、且能容忍每次升級重做（<a href="../component-boundary-and-js-impact/">#13</a> 的安全規則必看）</li>
</ul>
<h3 id="dfork--patch-組件-source">D：Fork / patch 組件 source</h3>
<ul>
<li><strong>機制</strong>：fork 整個組件、改 source code、自家維護版本</li>
<li><strong>成本特別高的原因</strong>：每次升級都要 merge upstream、客製永久綁在 fork</li>
<li><strong>D 才合理的情境</strong>：客製需求超過所有其他選項、且願意承擔 fork 維護成本</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>下次遇到組件整合任務、看到這些訊號就走「找邊界」的路：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>對應邊界類型</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>組件抓到的資料超過你想要的範圍</td>
          <td>索引邊界</td>
          <td>找組件的 <code>root</code> / <code>scope</code> / <code>selector</code> 參數</td>
      </tr>
      <tr>
          <td>同一段 CSS 在不同位置表現不一致</td>
          <td>重置邊界</td>
          <td>比對兩個位置的 ancestor class、看 reset 是否還在</td>
      </tr>
      <tr>
          <td><code>!important</code> 寫了還是沒蓋過</td>
          <td>specificity 邊界</td>
          <td>看組件 CSS 是否用 hash class 提升 specificity；考慮 CSS Layers</td>
      </tr>
      <tr>
          <td>組件渲染後客製 UI 消失</td>
          <td>DOM 結構邊界</td>
          <td>把客製 UI 搬出組件、用 CSS 控制視覺位置</td>
      </tr>
      <tr>
          <td>組件升級後客製失效</td>
          <td>內部邏輯邊界</td>
          <td>把客製重寫到組件介面層</td>
      </tr>
  </tbody>
</table>
<p><strong>使用順序</strong>：訊號出現 → 對應邊界 → 嘗試該邊界提供的介面 → 介面不夠用、才考慮往內推一層。</p>
]]></content:encoded></item><item><title>跨 viewport 雙模式 UI 的物理空間預算</title><link>https://tarrragon.github.io/blog/report/viewport-dual-mode-spatial-budget/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/viewport-dual-mode-spatial-budget/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>雙模式 UI 的 breakpoint 是「物理空間預算的結果」、不是「設計選擇」。&lt;/strong> 把每個元件的固有尺寸與 gap 加總、得到「兩種模式各自能存活的最小 viewport」 — breakpoint 從這個數字往上取一個安全餘裕。憑感覺取（768、1024）會在中間過渡區看到元件擠壓、消失或溢出。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>breakpoint 數字怎麼推算&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>stateful UI 怎麼跨兩個 slot 共用同一份節點&lt;/strong>由 &lt;a href="../pattern-cross-slot-node-relocation/">#54 跨 slot 同節點搬遷 pattern&lt;/a> 處理（兩議題機制不同：本篇是數字計算、#54 是 state 一致性）&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;hr>
&lt;h2 id="breakpoint-從元件預算推算">breakpoint 從元件預算推算&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Responsive UI 的本質是「同一份內容、在不同寬度有不同的呈現方式」。每種模式（mobile / tablet / desktop）對應一個 layout、每個 layout 有自己的最小可生存寬度 — 這是元件尺寸與 gap 的加總、不是任意選擇。&lt;/p>
&lt;p>當 breakpoint 取得比實際所需大、模式切換點與「真正放得下」的點不一致、使用者在過渡區看到擠壓、溢出、或元件神秘消失。&lt;/p>
&lt;h3 id="預算推算的步驟">預算推算的步驟&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>步驟&lt;/th>
 &lt;th>動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>列出該模式下所有可見元件的固有寬度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>列出元件之間的 gap 與 container 的 padding&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>加總得到「該模式所需的最小 viewport」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>breakpoint = 最小 viewport + 安全餘裕（避免邊界情況閃爍）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="這次任務的實際預算">這次任務的實際預算&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁的 desktop layout 包含：&lt;/p>
&lt;ul>
&lt;li>main 內容欄寬度 = 70ch ≈ 720px（theme 預設）&lt;/li>
&lt;li>filter sidebar 寬度 = 400px（自訂）&lt;/li>
&lt;li>main 與 filter 之間 gap = 32px&lt;/li>
&lt;li>body 左右 padding 各 64px = 128px&lt;/li>
&lt;/ul>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 filter 放在 main 左外側、main 維持置中時、所需最小 viewport：&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">最小 viewport ≈ main + 2 × (filter + gap) + body padding
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> = 720 + 2 × (400 + 32) + 128
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> = 1712px&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但這是「左右對稱都放下」的條件。若允許 filter 溢出 body padding（仍在 viewport 內可見）、條件放寬為：&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">最小 viewport ≈ main + filter + gap + body padding
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> = 720 + 400 + 32 + 128
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> = 1280px&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>選 1280 為下限、加餘裕後取 1400px。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>





&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="p">@&lt;/span>&lt;span class="k">media&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nt">min-width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">1400px&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">block&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 桌面寬模式 */&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下方寬度時 filter 顯示在 pagefind 原生位置（drawer 內、結果上方）、由 pagefind 自己處理。&lt;/p>
&lt;hr>
&lt;h2 id="設計取捨餘裕的取法">設計取捨：餘裕的取法&lt;/h2>
&lt;p>預算算出最小 viewport 後、breakpoint 加多少餘裕？四種做法：&lt;/p>
&lt;h3 id="a加-10-餘裕這個專案的預設">A：加 ~10% 餘裕（這個專案的預設）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：1280 + 120 ≈ 1400&lt;/li>
&lt;li>&lt;strong>選 A 的理由&lt;/strong>：避免邊界情況閃爍（使用者視窗剛好在 1280 像素時、輕微 resize 會反覆觸發切換）&lt;/li>
&lt;li>&lt;strong>適合&lt;/strong>：一般情境&lt;/li>
&lt;li>&lt;strong>代價&lt;/strong>：1280-1400 區間其實能放下、但 CSS 仍走 mobile 模式&lt;/li>
&lt;/ul>
&lt;h3 id="b取整到常見裝置寬度">B：取整到常見裝置寬度&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：1280 → 1366（MacBook Pro 寬度）或 1440（外接螢幕常見）&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：B 對齊裝置生態、A 對齊計算結果；B 對「設計給特定裝置的網站」較合理&lt;/li>
&lt;li>&lt;strong>B 比 A 好的情境&lt;/strong>：使用者群明確（公司內部工具、特定裝置網站）&lt;/li>
&lt;/ul>
&lt;h3 id="c完全等於最小值無餘裕">C：完全等於最小值（無餘裕）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：breakpoint = 1280&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：C 把所有可放下的視窗都納入 desktop 模式、A 留空間給邊界閃爍&lt;/li>
&lt;li>&lt;strong>C 才合理的情境&lt;/strong>：實作上有 debounce 處理閃爍、且裝置寬度集中在某個值附近&lt;/li>
&lt;/ul>
&lt;h3 id="d用-container-query-取代-viewport-breakpoint">D：用 container query 取代 viewport breakpoint&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：CSS Container Queries — 元件根據父容器寬度切換、跟 viewport 解耦&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：D 更精準（容器寬度才是元件真正可用的空間）、A 簡單（viewport 是全局觀念）&lt;/li>
&lt;li>&lt;strong>D 比 A 好的情境&lt;/strong>：元件可能放在不同寬度的容器內（CMS 系統、可嵌入元件）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="不該套用物理空間預算的情境">不該套用「物理空間預算」的情境&lt;/h2>
&lt;p>預算法有適用邊界：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>雙模式 UI 的 breakpoint 是「物理空間預算的結果」、不是「設計選擇」。</strong> 把每個元件的固有尺寸與 gap 加總、得到「兩種模式各自能存活的最小 viewport」 — breakpoint 從這個數字往上取一個安全餘裕。憑感覺取（768、1024）會在中間過渡區看到元件擠壓、消失或溢出。</p>
<blockquote>
<p>本篇焦點：<strong>breakpoint 數字怎麼推算</strong>。</p>
<ul>
<li><strong>stateful UI 怎麼跨兩個 slot 共用同一份節點</strong>由 <a href="../pattern-cross-slot-node-relocation/">#54 跨 slot 同節點搬遷 pattern</a> 處理（兩議題機制不同：本篇是數字計算、#54 是 state 一致性）</li>
</ul></blockquote>
<hr>
<h2 id="breakpoint-從元件預算推算">breakpoint 從元件預算推算</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Responsive UI 的本質是「同一份內容、在不同寬度有不同的呈現方式」。每種模式（mobile / tablet / desktop）對應一個 layout、每個 layout 有自己的最小可生存寬度 — 這是元件尺寸與 gap 的加總、不是任意選擇。</p>
<p>當 breakpoint 取得比實際所需大、模式切換點與「真正放得下」的點不一致、使用者在過渡區看到擠壓、溢出、或元件神秘消失。</p>
<h3 id="預算推算的步驟">預算推算的步驟</h3>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>列出該模式下所有可見元件的固有寬度</td>
      </tr>
      <tr>
          <td>2</td>
          <td>列出元件之間的 gap 與 container 的 padding</td>
      </tr>
      <tr>
          <td>3</td>
          <td>加總得到「該模式所需的最小 viewport」</td>
      </tr>
      <tr>
          <td>4</td>
          <td>breakpoint = 最小 viewport + 安全餘裕（避免邊界情況閃爍）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="這次任務的實際預算">這次任務的實際預算</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁的 desktop layout 包含：</p>
<ul>
<li>main 內容欄寬度 = 70ch ≈ 720px（theme 預設）</li>
<li>filter sidebar 寬度 = 400px（自訂）</li>
<li>main 與 filter 之間 gap = 32px</li>
<li>body 左右 padding 各 64px = 128px</li>
</ul>
<h3 id="判讀">判讀</h3>
<p>把 filter 放在 main 左外側、main 維持置中時、所需最小 viewport：</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">最小 viewport ≈ main + 2 × (filter + gap) + body padding
</span></span><span class="line"><span class="ln">2</span><span class="cl">             = 720 + 2 × (400 + 32) + 128
</span></span><span class="line"><span class="ln">3</span><span class="cl">             = 1712px</span></span></code></pre></div><p>但這是「左右對稱都放下」的條件。若允許 filter 溢出 body padding（仍在 viewport 內可見）、條件放寬為：</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">最小 viewport ≈ main + filter + gap + body padding
</span></span><span class="line"><span class="ln">2</span><span class="cl">             = 720 + 400 + 32 + 128
</span></span><span class="line"><span class="ln">3</span><span class="cl">             = 1280px</span></span></code></pre></div><p>選 1280 為下限、加餘裕後取 1400px。</p>
<h3 id="執行">執行</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="p">@</span><span class="k">media</span> <span class="o">(</span><span class="nt">min-width</span><span class="o">:</span> <span class="nt">1400px</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">block</span><span class="p">;</span> <span class="c">/* 桌面寬模式 */</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>下方寬度時 filter 顯示在 pagefind 原生位置（drawer 內、結果上方）、由 pagefind 自己處理。</p>
<hr>
<h2 id="設計取捨餘裕的取法">設計取捨：餘裕的取法</h2>
<p>預算算出最小 viewport 後、breakpoint 加多少餘裕？四種做法：</p>
<h3 id="a加-10-餘裕這個專案的預設">A：加 ~10% 餘裕（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：1280 + 120 ≈ 1400</li>
<li><strong>選 A 的理由</strong>：避免邊界情況閃爍（使用者視窗剛好在 1280 像素時、輕微 resize 會反覆觸發切換）</li>
<li><strong>適合</strong>：一般情境</li>
<li><strong>代價</strong>：1280-1400 區間其實能放下、但 CSS 仍走 mobile 模式</li>
</ul>
<h3 id="b取整到常見裝置寬度">B：取整到常見裝置寬度</h3>
<ul>
<li><strong>機制</strong>：1280 → 1366（MacBook Pro 寬度）或 1440（外接螢幕常見）</li>
<li><strong>跟 A 的取捨</strong>：B 對齊裝置生態、A 對齊計算結果；B 對「設計給特定裝置的網站」較合理</li>
<li><strong>B 比 A 好的情境</strong>：使用者群明確（公司內部工具、特定裝置網站）</li>
</ul>
<h3 id="c完全等於最小值無餘裕">C：完全等於最小值（無餘裕）</h3>
<ul>
<li><strong>機制</strong>：breakpoint = 1280</li>
<li><strong>跟 A 的取捨</strong>：C 把所有可放下的視窗都納入 desktop 模式、A 留空間給邊界閃爍</li>
<li><strong>C 才合理的情境</strong>：實作上有 debounce 處理閃爍、且裝置寬度集中在某個值附近</li>
</ul>
<h3 id="d用-container-query-取代-viewport-breakpoint">D：用 container query 取代 viewport breakpoint</h3>
<ul>
<li><strong>機制</strong>：CSS Container Queries — 元件根據父容器寬度切換、跟 viewport 解耦</li>
<li><strong>跟 A 的取捨</strong>：D 更精準（容器寬度才是元件真正可用的空間）、A 簡單（viewport 是全局觀念）</li>
<li><strong>D 比 A 好的情境</strong>：元件可能放在不同寬度的容器內（CMS 系統、可嵌入元件）</li>
</ul>
<hr>
<h2 id="不該套用物理空間預算的情境">不該套用「物理空間預算」的情境</h2>
<p>預算法有適用邊界：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內容靜態、不依設計尺寸（純文字段落）</td>
          <td>內容自然 reflow、不需要 breakpoint</td>
      </tr>
      <tr>
          <td>流體 layout（純 % / fr 單位）</td>
          <td>元件自動撐滿可用空間、無「最小寬度」概念</td>
      </tr>
      <tr>
          <td>完全 mobile-first 設計</td>
          <td>沒有「desktop 模式」這個分支</td>
      </tr>
      <tr>
          <td>元件尺寸 runtime 才知道</td>
          <td>用 ResizeObserver 動態調整、不是 breakpoint</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：兩種模式之間有「明確的視覺結構切換」嗎？是 → 用預算法；否 → 用流體 layout 或動態量測。</p>
<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>breakpoint 數字是 fact、CSS 變數住址唯一才能集中管理</td>
      </tr>
      <tr>
          <td><a href="../measurement-completeness/">#7 量測值缺一不可</a></td>
          <td>預算的每個分量都要有明確來源（寫死 / 量測）、不能估算</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>中間寬度時 UI 元件擠壓或重疊</td>
          <td>breakpoint 比實際所需小</td>
          <td>算物理空間預算、確認 breakpoint 對應的最小 viewport</td>
      </tr>
      <tr>
          <td>元件在某些寬度下消失但 CSS <code>display</code> 是 <code>block</code></td>
          <td>元件被 absolute 定位推出 viewport</td>
          <td>檢查 absolute 元件相對 viewport 的座標、是否為負</td>
      </tr>
      <tr>
          <td>Breakpoint 取常見值（768 / 1024）就壞</td>
          <td>那些值跟你的元件尺寸無關</td>
          <td>重算預算、不要用「常見」值</td>
      </tr>
      <tr>
          <td>Resize 過 breakpoint 時 layout 跳動</td>
          <td>沒加餘裕、邊界震盪</td>
          <td>加 10% 餘裕避開閃爍區</td>
      </tr>
  </tbody>
</table>
<p><strong>順序</strong>：看到擠壓或消失先量空間、不要立刻調 breakpoint 數字。數字背後有計算才不會反覆試錯。</p>
]]></content:encoded></item><item><title>視覺對齊用單一真實來源</title><link>https://tarrragon.github.io/blog/report/visual-alignment-single-source-of-truth/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/visual-alignment-single-source-of-truth/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>多個元素要對齊，每個元素的尺寸都需要「來源明確的數字」 — 寫死的 token 或 runtime 量測寫回變數，二選一不要混搭。&lt;/strong> 任何一個值含糊（猜的、估的、依字型自然渲染的），整條對齊基準就靠不住、修一處要找十處跟著改。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼對齊需要單一來源">為什麼對齊需要單一來源&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>對齊問題本質是&lt;strong>線性方程組&lt;/strong>：每個參與對齊的元素貢獻一個未知數，要解出對齊的 padding / margin / position 等變數，所有未知數都要有確定值。任一個未知數憑感覺給，整組無解。&lt;/p>
&lt;p>CSS 變數提供「一處定義、多處引用」的單一來源 — 改 token 只動一個值、所有引用點自動跟上。&lt;/p>
&lt;h3 id="兩種值來源各用對應的定義方法">兩種值來源、各用對應的定義方法&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>值的性質&lt;/th>
 &lt;th>確定方式&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>設計可決定的固定值&lt;/td>
 &lt;td>CSS 變數寫死&lt;/td>
 &lt;td>H1 height、icon size&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime 依字型 / 內容變動&lt;/td>
 &lt;td>ResizeObserver 量測寫回 CSS 變數&lt;/td>
 &lt;td>多行文字區塊高度、圖片自適應高度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>混搭的後果：寫死值跟實際渲染不一致時，對齊只在某些字型 / 螢幕 / 瀏覽器下成立、其他情境壞掉、且難以重現。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際應用">這次任務的實際應用&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁有四處要共用同一組視覺 token：&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>H1「搜尋」&lt;/td>
 &lt;td>自身 height&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pagefind search input&lt;/td>
 &lt;td>自身 height&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter sidebar &lt;code>padding-top&lt;/code>&lt;/td>
 &lt;td>對齊到 results 頂端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Drawer &lt;code>margin-top&lt;/code>&lt;/td>
 &lt;td>為 scope UI 讓出空間&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>H1 與 input 的 height 是設計可決定的固定值 — 用 CSS 變數寫死。Scope UI 的 height 受字型 / 換行影響、不可預測 — 用 ResizeObserver 量測寫回。&lt;/p>
&lt;h3 id="執行css-變數定義">執行：CSS 變數定義&lt;/h3>





&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="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>&lt;/span>&lt;span class="line">&lt;span class="ln">2&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">/* 設計決定 */&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-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">/* pagefind input 64 + border 4，鎖定 scale=1.0 */&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-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">/* drawer margin-top */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c">/* --search-scope-h 由 JS 量測寫入 :root */&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;/code>&lt;/pre>&lt;/div>&lt;h3 id="執行js-量測寫回">執行：JS 量測寫回&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">56&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="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="執行多處引用">執行：多處引用&lt;/h3>





&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="p">.&lt;/span>&lt;span class="nc">search-title&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">height&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="p">}&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="nc">search-shell&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__drawer&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">margin-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">scope&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="mi">8&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="p">.&lt;/span>&lt;span class="nc">search-filter-slot&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="k">padding-top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">calc&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="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>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &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">scope&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="mi">8&lt;/span>&lt;span class="kt">px&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">7&lt;/span>&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫死值與實際渲染不一致&lt;/td>
 &lt;td>字型變動或 scale 改變後對不齊&lt;/td>
 &lt;td>沒控制渲染參數（如 pagefind scale）&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;strong>確認每個值的性質、按性質選來源&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="鎖定渲染參數讓寫死值生效">鎖定渲染參數讓寫死值生效&lt;/h2>
&lt;p>當值「理論上可預測」但需要強制條件時，鎖定渲染參數。&lt;/p>
&lt;p>&lt;strong>核心定義&lt;/strong>：Pagefind input 高度 = &lt;code>64px × --pagefind-ui-scale&lt;/code>。把 scale 設成 1.0、input 自然渲染為 64px、加 border 4px 共 68px、剛好等於我們的 &lt;code>--search-form-h&lt;/code>。&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="p">.&lt;/span>&lt;span class="nc">search-shell&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&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>把組件提供的 scale 變數拉進自家設計系統 — 組件配合我們、不是我們配合組件。&lt;/p>
&lt;hr>
&lt;h2 id="設計取捨對齊基準的值來源策略">設計取捨：對齊基準的值來源策略&lt;/h2>
&lt;p>四種做法、各自機會成本不同。這個專案選 A（CSS 變數寫死 + var 引用）當固定值預設、B（ResizeObserver 量測寫回變數）當 runtime 值預設、其他做法是反模式。&lt;/p>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../single-source-of-truth/">#44 SSoT&lt;/a> 抽象原則在「對齊基準」這個面向的應用。&lt;/p>&lt;/blockquote>
&lt;h3 id="acss-變數寫死--var-引用這個專案對固定值的預設">A：CSS 變數寫死 + var() 引用（這個專案對固定值的預設）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：&lt;code>body.page-search { --search-title-h: 64px }&lt;/code>、其他地方用 &lt;code>var(--search-title-h)&lt;/code> 引用&lt;/li>
&lt;li>&lt;strong>選 A 的理由&lt;/strong>：定義住址唯一、改 token 自動跟上、無 runtime 開銷&lt;/li>
&lt;li>&lt;strong>適合&lt;/strong>：設計可決定的固定值（H1 height、icon size、間距）&lt;/li>
&lt;li>&lt;strong>代價&lt;/strong>：值不能跟 runtime 內容變動 — 字型大幅變化時 layout 可能不適配&lt;/li>
&lt;/ul>
&lt;h3 id="bresizeobserver-量測寫回變數這個專案對-runtime-值的預設">B：ResizeObserver 量測寫回變數（這個專案對 runtime 值的預設）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：JS 量測元素實際渲染高度、&lt;code>setProperty('--search-scope-h', ...)&lt;/code> 寫回變數&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：B 自動跟著實際渲染走、A 假設渲染條件穩定；B 多 JS 一層、A 純 CSS&lt;/li>
&lt;li>&lt;strong>B 比 A 好的情境&lt;/strong>：值受字型 / 換行 / 內容動態影響、無法 build time 預測&lt;/li>
&lt;/ul>
&lt;h3 id="c複製-magic-number-在多處">C：複製 magic number 在多處&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：&lt;code>padding-top: 152px&lt;/code> 在多個 selector 直接寫死&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：C 寫法直接、不用變數系統；但改一處要找全部、漏改一個就壞&lt;/li>
&lt;li>&lt;strong>C 是反模式&lt;/strong>：magic number 是「未來 debug 的潛在炸彈」（&lt;a href="../single-source-of-truth/">#44 SSoT&lt;/a>） — 改一處要找全部、漏改一個就壞&lt;/li>
&lt;/ul>
&lt;h3 id="d估算值不寫變數不量測">D：估算值（不寫變數、不量測）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：執行者依感覺寫「padding-top: 152px、應該對齊」&lt;/li>
&lt;li>&lt;strong>成本特別高的原因&lt;/strong>：估值對 = 巧合、估值錯 = 看起來對但 +/- 幾 px、後者更糟（錯誤被視覺接受、不會被發現）&lt;/li>
&lt;li>&lt;strong>D 是反模式&lt;/strong>：任何寫死值都該標明來源（fact / 鎖定條件 / 量測）— 估值對 = 巧合、估值錯 = 看起來對但實際 +/- 幾 px、ship 後在邊界情境暴露&lt;/li>
&lt;/ul>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>改一個 CSS 數字、要去 N 個地方跟著改&lt;/td>
 &lt;td>缺少單一來源&lt;/td>
 &lt;td>找出複製的 magic number、提成 CSS 變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設計稿對齊但實作對不齊&lt;/td>
 &lt;td>把可變值當固定值&lt;/td>
 &lt;td>量測該元素的真實 height、決定改用 ResizeObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>字型變動或 dark mode 後對齊壞掉&lt;/td>
 &lt;td>寫死值依賴某個沒鎖定的渲染參數&lt;/td>
 &lt;td>找出該渲染參數、用 CSS 變數鎖定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對齊「大部分時候對」、邊界 case 壞掉&lt;/td>
 &lt;td>沒處理動態高度&lt;/td>
 &lt;td>把該值用 ResizeObserver 量測寫回&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>多個元素要對齊，每個元素的尺寸都需要「來源明確的數字」 — 寫死的 token 或 runtime 量測寫回變數，二選一不要混搭。</strong> 任何一個值含糊（猜的、估的、依字型自然渲染的），整條對齊基準就靠不住、修一處要找十處跟著改。</p>
<hr>
<h2 id="為什麼對齊需要單一來源">為什麼對齊需要單一來源</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>對齊問題本質是<strong>線性方程組</strong>：每個參與對齊的元素貢獻一個未知數，要解出對齊的 padding / margin / position 等變數，所有未知數都要有確定值。任一個未知數憑感覺給，整組無解。</p>
<p>CSS 變數提供「一處定義、多處引用」的單一來源 — 改 token 只動一個值、所有引用點自動跟上。</p>
<h3 id="兩種值來源各用對應的定義方法">兩種值來源、各用對應的定義方法</h3>
<table>
  <thead>
      <tr>
          <th>值的性質</th>
          <th>確定方式</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計可決定的固定值</td>
          <td>CSS 變數寫死</td>
          <td>H1 height、icon size</td>
      </tr>
      <tr>
          <td>Runtime 依字型 / 內容變動</td>
          <td>ResizeObserver 量測寫回 CSS 變數</td>
          <td>多行文字區塊高度、圖片自適應高度</td>
      </tr>
  </tbody>
</table>
<p>混搭的後果：寫死值跟實際渲染不一致時，對齊只在某些字型 / 螢幕 / 瀏覽器下成立、其他情境壞掉、且難以重現。</p>
<hr>
<h2 id="這次任務的實際應用">這次任務的實際應用</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁有四處要共用同一組視覺 token：</p>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>為什麼需要這個值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>H1「搜尋」</td>
          <td>自身 height</td>
      </tr>
      <tr>
          <td>Pagefind search input</td>
          <td>自身 height</td>
      </tr>
      <tr>
          <td>Filter sidebar <code>padding-top</code></td>
          <td>對齊到 results 頂端</td>
      </tr>
      <tr>
          <td>Drawer <code>margin-top</code></td>
          <td>為 scope UI 讓出空間</td>
      </tr>
  </tbody>
</table>
<h3 id="判讀">判讀</h3>
<p>H1 與 input 的 height 是設計可決定的固定值 — 用 CSS 變數寫死。Scope UI 的 height 受字型 / 換行影響、不可預測 — 用 ResizeObserver 量測寫回。</p>
<h3 id="執行css-變數定義">執行：CSS 變數定義</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-title-h</span><span class="p">:</span> <span class="mi">64</span><span class="kt">px</span><span class="p">;</span>     <span class="c">/* 設計決定 */</span>
</span></span><span class="line"><span class="ln">3</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">/* pagefind input 64 + border 4，鎖定 scale=1.0 */</span>
</span></span><span class="line"><span class="ln">4</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">/* drawer margin-top */</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c">/* --search-scope-h 由 JS 量測寫入 :root */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="執行js-量測寫回">執行：JS 量測寫回</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">syncScopeHeight</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">h</span> <span class="o">=</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">||</span> <span class="mi">56</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</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">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">syncScopeHeight</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">syncScopeHeight</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</span></span></span></code></pre></div><h3 id="執行多處引用">執行：多處引用</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="p">.</span><span class="nc">search-title</span> <span class="p">{</span> <span class="k">height</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="p">}</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="nc">pagefind-ui__drawer</span> <span class="p">{</span> <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">scope</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 class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">padding-top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <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">6</span><span class="cl">    <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">scope</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="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">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></code></pre></div><hr>
<h2 id="對齊問題的兩種失敗模式">對齊問題的兩種失敗模式</h2>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>表現</th>
          <th>根因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫死值與實際渲染不一致</td>
          <td>字型變動或 scale 改變後對不齊</td>
          <td>沒控制渲染參數（如 pagefind scale）</td>
      </tr>
      <tr>
          <td>用估算值代替量測</td>
          <td>邊界情境（短/長文字、特殊字型）壞掉</td>
          <td>把不可預測的值當固定值處理</td>
      </tr>
  </tbody>
</table>
<p>兩者共通的修法是：<strong>確認每個值的性質、按性質選來源</strong>。</p>
<hr>
<h2 id="鎖定渲染參數讓寫死值生效">鎖定渲染參數讓寫死值生效</h2>
<p>當值「理論上可預測」但需要強制條件時，鎖定渲染參數。</p>
<p><strong>核心定義</strong>：Pagefind input 高度 = <code>64px × --pagefind-ui-scale</code>。把 scale 設成 1.0、input 自然渲染為 64px、加 border 4px 共 68px、剛好等於我們的 <code>--search-form-h</code>。</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="p">.</span><span class="nc">search-shell</span> <span class="p">{</span> <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>把組件提供的 scale 變數拉進自家設計系統 — 組件配合我們、不是我們配合組件。</p>
<hr>
<h2 id="設計取捨對齊基準的值來源策略">設計取捨：對齊基準的值來源策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（CSS 變數寫死 + var 引用）當固定值預設、B（ResizeObserver 量測寫回變數）當 runtime 值預設、其他做法是反模式。</p>
<blockquote>
<p>本篇是 <a href="../single-source-of-truth/">#44 SSoT</a> 抽象原則在「對齊基準」這個面向的應用。</p></blockquote>
<h3 id="acss-變數寫死--var-引用這個專案對固定值的預設">A：CSS 變數寫死 + var() 引用（這個專案對固定值的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>body.page-search { --search-title-h: 64px }</code>、其他地方用 <code>var(--search-title-h)</code> 引用</li>
<li><strong>選 A 的理由</strong>：定義住址唯一、改 token 自動跟上、無 runtime 開銷</li>
<li><strong>適合</strong>：設計可決定的固定值（H1 height、icon size、間距）</li>
<li><strong>代價</strong>：值不能跟 runtime 內容變動 — 字型大幅變化時 layout 可能不適配</li>
</ul>
<h3 id="bresizeobserver-量測寫回變數這個專案對-runtime-值的預設">B：ResizeObserver 量測寫回變數（這個專案對 runtime 值的預設）</h3>
<ul>
<li><strong>機制</strong>：JS 量測元素實際渲染高度、<code>setProperty('--search-scope-h', ...)</code> 寫回變數</li>
<li><strong>跟 A 的取捨</strong>：B 自動跟著實際渲染走、A 假設渲染條件穩定；B 多 JS 一層、A 純 CSS</li>
<li><strong>B 比 A 好的情境</strong>：值受字型 / 換行 / 內容動態影響、無法 build time 預測</li>
</ul>
<h3 id="c複製-magic-number-在多處">C：複製 magic number 在多處</h3>
<ul>
<li><strong>機制</strong>：<code>padding-top: 152px</code> 在多個 selector 直接寫死</li>
<li><strong>跟 A 的取捨</strong>：C 寫法直接、不用變數系統；但改一處要找全部、漏改一個就壞</li>
<li><strong>C 是反模式</strong>：magic number 是「未來 debug 的潛在炸彈」（<a href="../single-source-of-truth/">#44 SSoT</a>） — 改一處要找全部、漏改一個就壞</li>
</ul>
<h3 id="d估算值不寫變數不量測">D：估算值（不寫變數、不量測）</h3>
<ul>
<li><strong>機制</strong>：執行者依感覺寫「padding-top: 152px、應該對齊」</li>
<li><strong>成本特別高的原因</strong>：估值對 = 巧合、估值錯 = 看起來對但 +/- 幾 px、後者更糟（錯誤被視覺接受、不會被發現）</li>
<li><strong>D 是反模式</strong>：任何寫死值都該標明來源（fact / 鎖定條件 / 量測）— 估值對 = 巧合、估值錯 = 看起來對但實際 +/- 幾 px、ship 後在邊界情境暴露</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改一個 CSS 數字、要去 N 個地方跟著改</td>
          <td>缺少單一來源</td>
          <td>找出複製的 magic number、提成 CSS 變數</td>
      </tr>
      <tr>
          <td>設計稿對齊但實作對不齊</td>
          <td>把可變值當固定值</td>
          <td>量測該元素的真實 height、決定改用 ResizeObserver</td>
      </tr>
      <tr>
          <td>字型變動或 dark mode 後對齊壞掉</td>
          <td>寫死值依賴某個沒鎖定的渲染參數</td>
          <td>找出該渲染參數、用 CSS 變數鎖定</td>
      </tr>
      <tr>
          <td>對齊「大部分時候對」、邊界 case 壞掉</td>
          <td>沒處理動態高度</td>
          <td>把該值用 ResizeObserver 量測寫回</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：對齊不是視覺問題，是「每個參與元素是否有確定尺寸」的問題。任何一個值不確定、整組對齊就脆弱。</p>
]]></content:encoded></item><item><title>拓樸理解先行於 CSS 規則</title><link>https://tarrragon.github.io/blog/report/dom-topology-before-css/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/dom-topology-before-css/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>CSS 是基於 DOM tree 的規則系統 — 不知道 tree 的真實結構，寫的 CSS 規則無法生效。&lt;/strong> 看 class name 的命名規則（如 &lt;code>__form&lt;/code>、&lt;code>__drawer&lt;/code> 看起來像 sibling）容易推錯層級；寫 CSS 之前用工具直接讀 live DOM tree、確認哪些是 grid item、哪些是 grid item 內部的子元素。&lt;/p>
&lt;hr>
&lt;h2 id="層級必須從-live-dom-讀">層級必須從 live DOM 讀&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS class name 是「用途標記」、不是「結構描述」。&lt;code>.parent__child&lt;/code> 這種 BEM 風格在很多框架裡只是作者方便辨認用途，跟元素之間的 DOM parent-child 關係無對應。&lt;/p>
&lt;p>當作者在 wrapper 裡又加一層 wrapper，class name 不一定改 — 同一個 class name 在不同框架版本可能對應不同的 DOM 巢狀。&lt;/p>
&lt;p>唯一能確定 DOM 層級的方法是&lt;strong>讀 live DOM&lt;/strong>。&lt;/p>
&lt;h3 id="看-dom-的工具選擇">看 DOM 的工具選擇&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>限制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>瀏覽器 DevTools Elements 面板&lt;/td>
 &lt;td>手動探索、單次確認&lt;/td>
 &lt;td>截圖溝通慢、不能寫成測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>playwright browser_evaluate&lt;/code>&lt;/td>
 &lt;td>程式化讀 parent chain、computed style、bounding rect&lt;/td>
 &lt;td>需要 server 跑著&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>框架原始碼（svelte template、JSX）&lt;/td>
 &lt;td>確認靜態 DOM 結構&lt;/td>
 &lt;td>動態渲染情境看不到&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優先用 playwright — 同一段 query 可以重複跑、結果可以寫進測試。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的拓樸誤判">這次任務的拓樸誤判&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把 search scope UI 放在「搜尋輸入框與結果之間」。基於 class name 推測 DOM 結構：&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">.pagefind-ui
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── .pagefind-ui__form ← 搜尋輸入框
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">└── .pagefind-ui__drawer ← 結果（與 filter）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Class name &lt;code>__form&lt;/code> 與 &lt;code>__drawer&lt;/code> 都用 &lt;code>__&lt;/code> 前綴、並列在 &lt;code>.pagefind-ui&lt;/code> 下、看起來是 sibling。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>依此假設寫 CSS Grid：把 &lt;code>.pagefind-ui&lt;/code> 設為 grid、用 &lt;code>display: contents&lt;/code> 串接、把 form 放 row 2、scope 放 row 3、drawer 放 row 4。&lt;/p>
&lt;p>實際渲染後：scope 跑到頁尾。&lt;/p>
&lt;p>用 &lt;code>playwright browser_evaluate&lt;/code> 讀 live DOM tree：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">let&lt;/span> &lt;span class="nx">parents&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[],&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">drawer&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="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&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="nx">parents&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tagName&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parentElement&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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">DIV.pagefind-ui__drawer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">FORM.pagefind-ui__form ← drawer 在 form 內！
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">DIV.pagefind-ui
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">DIV#search&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>drawer 是 form 的 child、不是 sibling。我們的 grid 規則把 form（含 drawer 全部結果）放在 row 2、scope 放 row 3 — scope 自然跑到所有結果之後。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>
&lt;p>確認 DOM 後改用「scope absolute 浮在 form 上、drawer 用 margin-top 讓位」的策略 — 不再嘗試把 form 與 drawer 拆到不同 grid row。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>CSS 是基於 DOM tree 的規則系統 — 不知道 tree 的真實結構，寫的 CSS 規則無法生效。</strong> 看 class name 的命名規則（如 <code>__form</code>、<code>__drawer</code> 看起來像 sibling）容易推錯層級；寫 CSS 之前用工具直接讀 live DOM tree、確認哪些是 grid item、哪些是 grid item 內部的子元素。</p>
<hr>
<h2 id="層級必須從-live-dom-讀">層級必須從 live DOM 讀</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS class name 是「用途標記」、不是「結構描述」。<code>.parent__child</code> 這種 BEM 風格在很多框架裡只是作者方便辨認用途，跟元素之間的 DOM parent-child 關係無對應。</p>
<p>當作者在 wrapper 裡又加一層 wrapper，class name 不一定改 — 同一個 class name 在不同框架版本可能對應不同的 DOM 巢狀。</p>
<p>唯一能確定 DOM 層級的方法是<strong>讀 live DOM</strong>。</p>
<h3 id="看-dom-的工具選擇">看 DOM 的工具選擇</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>適用情境</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>瀏覽器 DevTools Elements 面板</td>
          <td>手動探索、單次確認</td>
          <td>截圖溝通慢、不能寫成測試</td>
      </tr>
      <tr>
          <td><code>playwright browser_evaluate</code></td>
          <td>程式化讀 parent chain、computed style、bounding rect</td>
          <td>需要 server 跑著</td>
      </tr>
      <tr>
          <td>框架原始碼（svelte template、JSX）</td>
          <td>確認靜態 DOM 結構</td>
          <td>動態渲染情境看不到</td>
      </tr>
  </tbody>
</table>
<p>優先用 playwright — 同一段 query 可以重複跑、結果可以寫進測試。</p>
<hr>
<h2 id="這次任務的拓樸誤判">這次任務的拓樸誤判</h2>
<h3 id="觀察">觀察</h3>
<p>要把 search scope UI 放在「搜尋輸入框與結果之間」。基於 class name 推測 DOM 結構：</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">.pagefind-ui
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── .pagefind-ui__form        ← 搜尋輸入框
</span></span><span class="line"><span class="ln">3</span><span class="cl">└── .pagefind-ui__drawer      ← 結果（與 filter）</span></span></code></pre></div><p>Class name <code>__form</code> 與 <code>__drawer</code> 都用 <code>__</code> 前綴、並列在 <code>.pagefind-ui</code> 下、看起來是 sibling。</p>
<h3 id="判讀">判讀</h3>
<p>依此假設寫 CSS Grid：把 <code>.pagefind-ui</code> 設為 grid、用 <code>display: contents</code> 串接、把 form 放 row 2、scope 放 row 3、drawer 放 row 4。</p>
<p>實際渲染後：scope 跑到頁尾。</p>
<p>用 <code>playwright browser_evaluate</code> 讀 live DOM tree：</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="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">2</span><span class="cl"><span class="kd">let</span> <span class="nx">parents</span> <span class="o">=</span> <span class="p">[],</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">drawer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nx">el</span> <span class="o">&amp;&amp;</span> <span class="nx">el</span> <span class="o">!==</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">parents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">tagName</span> <span class="o">+</span> <span class="s1">&#39;.&#39;</span> <span class="o">+</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">el</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">parentElement</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>結果：</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">DIV.pagefind-ui__drawer
</span></span><span class="line"><span class="ln">2</span><span class="cl">FORM.pagefind-ui__form        ← drawer 在 form 內！
</span></span><span class="line"><span class="ln">3</span><span class="cl">DIV.pagefind-ui
</span></span><span class="line"><span class="ln">4</span><span class="cl">DIV#search</span></span></code></pre></div><p>drawer 是 form 的 child、不是 sibling。我們的 grid 規則把 form（含 drawer 全部結果）放在 row 2、scope 放 row 3 — scope 自然跑到所有結果之後。</p>
<h3 id="執行">執行</h3>
<p>確認 DOM 後改用「scope absolute 浮在 form 上、drawer 用 margin-top 讓位」的策略 — 不再嘗試把 form 與 drawer 拆到不同 grid row。</p>
<hr>
<h2 id="內在屬性比較拓樸推理的可靠性">內在屬性比較：拓樸推理的可靠性</h2>
<table>
  <thead>
      <tr>
          <th>推理來源</th>
          <th>可靠性</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Live DOM（playwright / DevTools）</td>
          <td>最高 — 反映實際渲染</td>
          <td>Debug、整合外部組件</td>
      </tr>
      <tr>
          <td>框架 source / template</td>
          <td>高 — 靜態結構</td>
          <td>自家組件、可讀的 source</td>
      </tr>
      <tr>
          <td>Class name 命名規則</td>
          <td>低 — 命名是慣例、不是契約</td>
          <td>僅參考、不依賴</td>
      </tr>
      <tr>
          <td>視覺截圖推測</td>
          <td>最低 — 看不到 DOM 包裹層</td>
          <td>不應作為唯一依據</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>Live DOM &gt; source &gt; 命名 &gt; 視覺</strong>。Class name 與視覺只能形成假設、必須用前兩者驗證。</p>
<hr>
<h2 id="display-contents-的拓樸限制">display: contents 的拓樸限制</h2>
<p>當決定用 <code>display: contents</code> 串接讓子元素參與外層 grid，必須注意：<strong>contents 只能讓直接子節點上去、不能跨越多層 box</strong>。</p>
<p>例：要讓 form 內的 drawer 參與 search-shell 的 grid，需要 form 也設 <code>display: contents</code>。但 form 設 contents 後：</p>
<ul>
<li>form 自己的 box 消失</li>
<li>依賴 form 為 offset parent 的子元素（如 absolute 定位的 clear button）失去定位基準</li>
<li>form 的 <code>::before</code> / <code>::after</code> 偽元素可能不渲染</li>
</ul>
<p><strong>display: contents 適用條件</strong>：中間層 box 沒有自己的視覺責任（背景、邊框、定位、尺寸） — 否則拆開後視覺破壞。</p>
<hr>
<h2 id="設計取捨拓樸理解的方法">設計取捨：拓樸理解的方法</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（讀 live DOM）當預設、其他做法在特定情境合理。</p>
<h3 id="a讀-live-domplaywright--devtools這個專案的預設">A：讀 live DOM（playwright / DevTools）（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：用 <code>playwright browser_evaluate</code> 讀 ancestor chain、computed style、bounding rect；或 DevTools Elements 面板手動探索</li>
<li><strong>選 A 的理由</strong>：反映實際渲染結果、跨 framework 都對、可寫成測試</li>
<li><strong>適合</strong>：debug、整合外部組件、寫第一版 CSS 之前</li>
<li><strong>代價</strong>：需要 server 跑著（可用 hugo dev / static server）</li>
</ul>
<h3 id="b讀框架-source--template">B：讀框架 source / template</h3>
<ul>
<li><strong>機制</strong>：直接看 svelte / react component 的 template</li>
<li><strong>跟 A 的取捨</strong>：B 看靜態結構、A 看 runtime 結構；B 對自家組件夠用、對動態渲染（runtime wrapper / portal）會漏</li>
<li><strong>B 比 A 好的情境</strong>：自家組件、template 跟 DOM 1:1 對應、不需要 runtime 確認</li>
</ul>
<h3 id="c用-class-name-命名規則推測">C：用 class name 命名規則推測</h3>
<ul>
<li><strong>機制</strong>：看 <code>.parent__child</code> 推測 DOM 巢狀</li>
<li><strong>跟 A 的取捨</strong>：C 完全不需要工具、A 需要 server；但 C 命名是慣例不是契約、容易錯</li>
<li><strong>C 才合理的情境</strong>：初步假設、必須用 A/B 驗證後才能寫 CSS — 不應作為唯一依據</li>
</ul>
<h3 id="d視覺截圖推測">D：視覺截圖推測</h3>
<ul>
<li><strong>機制</strong>：看截圖猜 DOM 結構</li>
<li><strong>成本特別高的原因</strong>：截圖看不到 wrapper、看不到 display: contents 等不可視結構</li>
<li><strong>D 是反模式</strong>：視覺上看起來相同的 DOM 可能完全不同 — 截圖驗收的盲區會在規則寫了不生效時才被發現、debug 成本指數放大</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫好的 CSS 規則完全沒生效</td>
          <td>元素根本不在預期的 DOM 位置</td>
          <td>用 playwright <code>browser_evaluate</code> 讀 ancestor chain</td>
      </tr>
      <tr>
          <td>Grid / flex 排序與預期不符</td>
          <td>子元素不是直接 grid item</td>
          <td>確認 grid container 的 direct children</td>
      </tr>
      <tr>
          <td>設了 <code>display: contents</code> 後某些定位元素跑掉</td>
          <td>那層 box 是 absolute 元素的 offset parent</td>
          <td>把該層 box 留下、找其他方式達成 layout</td>
      </tr>
      <tr>
          <td>框架重繪後 layout 完全變了</td>
          <td>框架增加了 wrapper 元素</td>
          <td>重新讀 live DOM、更新 CSS 假設</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：CSS 行為與預期不符 ≥ 1 次，先回去看 DOM tree、不要繼續調 CSS 規則。先看才不會試錯。</p>
]]></content:encoded></item><item><title>客製 UI 留 framework 邊界外、用 CSS 控制視覺位置</title><link>https://tarrragon.github.io/blog/report/coexisting-with-framework-managed-dom/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/coexisting-with-framework-managed-dom/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>客製 UI 留在 framework 管轄的 DOM 邊界外、用 CSS（absolute、margin spacer、grid）達成想要的視覺位置。&lt;/strong> 注入 framework 子樹的客製元素會被 reconciliation 清掉、跟渲染週期競爭、行為不可預測。邊界外的客製跟 framework 解耦、命運由我們自己決定。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：客製 UI 該放哪。&lt;strong>framework 元件本身需要動（搬節點、改順序、改 attribute）的安全規則&lt;/strong>由 &lt;a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則&lt;/a> 處理。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼-framework-管轄區會清外來節點">為什麼 framework 管轄區會清外來節點&lt;/h2>
&lt;h3 id="reconciliation-機制">Reconciliation 機制&lt;/h3>
&lt;p>Svelte / React 等框架透過「component tree → DOM tree」的 reconciliation 機制保持 UI 與 state 同步。框架在 patch 時：&lt;/p>
&lt;ol>
&lt;li>比對 component tree 的當前狀態與目標狀態&lt;/li>
&lt;li>計算 DOM 需要的最小變動&lt;/li>
&lt;li>套用變動到實際 DOM&lt;/li>
&lt;/ol>
&lt;p>關鍵是步驟 2：&lt;strong>框架只認得自己 create 的節點&lt;/strong>。外來節點（我們手動 appendChild 進去的）不在它的 component tree 裡、被視為「該節點不該存在」、清掉。&lt;/p>
&lt;p>這不是 bug、是 reconciliation 的正常行為 — 框架要保證 DOM 跟 component state 一致、外來節點屬於不一致的部分。&lt;/p>
&lt;h3 id="外來節點的命運是不可預測的">外來節點的命運是不可預測的&lt;/h3>
&lt;p>不同框架 / 不同 reconciliation 策略對外來節點的處理：&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>Svelte&lt;/td>
 &lt;td>多數情境清掉、視 patch 點而定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>React&lt;/td>
 &lt;td>通常清掉（Virtual DOM diff 時）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vue&lt;/td>
 &lt;td>通常清掉、但 v-pre 包裹可保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Web Components&lt;/td>
 &lt;td>由 component 內部邏輯決定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>「不可預測」本身就是問題&lt;/strong> — 即使某次測試沒清、下次升級或 patch 時可能清。設計時不該依賴未明確保證的行為。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的具體情境">這次任務的具體情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把搜尋範圍切換 UI（scope radio group）放在「pagefind 搜尋輸入框與結果之間」 — 視覺上希望它就在 form 與 drawer 中間。&lt;/p>
&lt;p>第一次嘗試：JS 把 scope element 用 &lt;code>form.insertAdjacentElement('afterend', scopeEl)&lt;/code> 注入 &lt;code>.pagefind-ui&lt;/code> 內部。&lt;/p>
&lt;p>結果：使用者打字後 scope 消失。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>Pagefind 用 svelte 構建 UI、reactivity 監聽 search query 變動。Query 改變時 svelte 會 patch &lt;code>.pagefind-ui&lt;/code> 的子樹 — 我們注入的 scope 不是 svelte 認得的節點、被視為差異清掉。&lt;/p>
&lt;h3 id="執行邊界外--css-控制位置">執行：邊界外 + CSS 控制位置&lt;/h3>
&lt;p>策略改為「scope 留在 &lt;code>.search-shell&lt;/code> 裡（framework 邊界外）、用 CSS absolute 浮在 form 上」：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-shell&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- 邊界外、永不被清 --&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- pagefind 進來這裡 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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="p">.&lt;/span>&lt;span class="nc">search-shell&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">relative&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">2&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-scope&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="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&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="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="mi">4&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="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 class="p">.&lt;/span>&lt;span class="nc">search-shell&lt;/span> &lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__drawer&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="k">margin-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">scope&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="mi">8&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c">/* 為 scope 讓位 */&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>scope 不在 svelte 管轄區、永遠不被清；視覺位置靠 absolute + drawer 的 margin-top 共同決定。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>客製 UI 留在 framework 管轄的 DOM 邊界外、用 CSS（absolute、margin spacer、grid）達成想要的視覺位置。</strong> 注入 framework 子樹的客製元素會被 reconciliation 清掉、跟渲染週期競爭、行為不可預測。邊界外的客製跟 framework 解耦、命運由我們自己決定。</p>
<blockquote>
<p>本篇焦點：客製 UI 該放哪。<strong>framework 元件本身需要動（搬節點、改順序、改 attribute）的安全規則</strong>由 <a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a> 處理。</p></blockquote>
<hr>
<h2 id="為什麼-framework-管轄區會清外來節點">為什麼 framework 管轄區會清外來節點</h2>
<h3 id="reconciliation-機制">Reconciliation 機制</h3>
<p>Svelte / React 等框架透過「component tree → DOM tree」的 reconciliation 機制保持 UI 與 state 同步。框架在 patch 時：</p>
<ol>
<li>比對 component tree 的當前狀態與目標狀態</li>
<li>計算 DOM 需要的最小變動</li>
<li>套用變動到實際 DOM</li>
</ol>
<p>關鍵是步驟 2：<strong>框架只認得自己 create 的節點</strong>。外來節點（我們手動 appendChild 進去的）不在它的 component tree 裡、被視為「該節點不該存在」、清掉。</p>
<p>這不是 bug、是 reconciliation 的正常行為 — 框架要保證 DOM 跟 component state 一致、外來節點屬於不一致的部分。</p>
<h3 id="外來節點的命運是不可預測的">外來節點的命運是不可預測的</h3>
<p>不同框架 / 不同 reconciliation 策略對外來節點的處理：</p>
<table>
  <thead>
      <tr>
          <th>框架</th>
          <th>外來節點命運</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Svelte</td>
          <td>多數情境清掉、視 patch 點而定</td>
      </tr>
      <tr>
          <td>React</td>
          <td>通常清掉（Virtual DOM diff 時）</td>
      </tr>
      <tr>
          <td>Vue</td>
          <td>通常清掉、但 v-pre 包裹可保留</td>
      </tr>
      <tr>
          <td>Web Components</td>
          <td>由 component 內部邏輯決定</td>
      </tr>
  </tbody>
</table>
<p><strong>「不可預測」本身就是問題</strong> — 即使某次測試沒清、下次升級或 patch 時可能清。設計時不該依賴未明確保證的行為。</p>
<hr>
<h2 id="這次任務的具體情境">這次任務的具體情境</h2>
<h3 id="觀察">觀察</h3>
<p>要把搜尋範圍切換 UI（scope radio group）放在「pagefind 搜尋輸入框與結果之間」 — 視覺上希望它就在 form 與 drawer 中間。</p>
<p>第一次嘗試：JS 把 scope element 用 <code>form.insertAdjacentElement('afterend', scopeEl)</code> 注入 <code>.pagefind-ui</code> 內部。</p>
<p>結果：使用者打字後 scope 消失。</p>
<h3 id="判讀">判讀</h3>
<p>Pagefind 用 svelte 構建 UI、reactivity 監聽 search query 變動。Query 改變時 svelte 會 patch <code>.pagefind-ui</code> 的子樹 — 我們注入的 scope 不是 svelte 認得的節點、被視為差異清掉。</p>
<h3 id="執行邊界外--css-控制位置">執行：邊界外 + CSS 控制位置</h3>
<p>策略改為「scope 留在 <code>.search-shell</code> 裡（framework 邊界外）、用 CSS absolute 浮在 form 上」：</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="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-shell&#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">h1</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">h1</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 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">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;search&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>               <span class="c">&lt;!-- pagefind 進來這裡 --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><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="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">2</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">3</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">4</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="mi">4</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="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 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">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">scope</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 class="c">/* 為 scope 讓位 */</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>scope 不在 svelte 管轄區、永遠不被清；視覺位置靠 absolute + drawer 的 margin-top 共同決定。</p>
<hr>
<h2 id="css-達成視覺位置的設計工具">CSS 達成視覺位置的設計工具</h2>
<h3 id="工具-1absolute--容器-relative">工具 1：Absolute + 容器 relative</h3>
<p>把客製 UI 設 <code>position: absolute</code>、容器設 <code>position: relative</code> 當定位基準。</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="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">2</span><span class="cl"><span class="p">.</span><span class="nc">search-custom</span> <span class="p">{</span> <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span> <span class="k">top</span><span class="p">:</span> <span class="o">...</span><span class="p">;</span> <span class="k">left</span><span class="p">:</span> <span class="o">...</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>客製 UI 跟 framework 元素脫離 layout flow、各自獨立。</p>
<h3 id="工具-2margin-spacer-推開-framework-元素">工具 2：Margin spacer 推開 framework 元素</h3>
<p>要在 framework 元素之間插入空間放客製 UI、改 framework 元素的 margin / padding 推出空間：</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="p">.</span><span class="nc">framework-element</span> <span class="p">{</span> <span class="k">margin-top</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">custom</span><span class="o">-</span><span class="n">height</span><span class="p">);</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">custom-ui</span> <span class="p">{</span> <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span> <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="k">height</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">custom</span><span class="o">-</span><span class="n">height</span><span class="p">);</span> <span class="p">}</span></span></span></code></pre></div><p>framework 元素留出空間、客製 UI 浮在空間上。</p>
<h3 id="工具-3grid-容器讓-framework-元件當-grid-item">工具 3：Grid 容器讓 framework 元件當 grid item</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="p">.</span><span class="nc">search-shell</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="k">grid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">grid-template-rows</span><span class="p">:</span> <span class="kc">auto</span> <span class="kc">auto</span> <span class="mi">1</span><span class="n">fr</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="o">&gt;</span> <span class="p">.</span><span class="nc">search-scope</span> <span class="p">{</span> <span class="k">grid-row</span><span class="p">:</span> <span class="mi">2</span><span class="p">;</span> <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="o">&gt;</span> <span class="p">#</span><span class="nn">search</span> <span class="p">{</span> <span class="k">grid-row</span><span class="p">:</span> <span class="mi">3</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>把 framework 元件當 grid 的一個 item — grid 控制 layout、framework 不知道有 grid 在外層、繼續管它的子樹。</p>
<h3 id="工具-4用-css-variables-共享尺寸">工具 4：用 CSS variables 共享尺寸</h3>
<p>framework 元素的尺寸需要參考客製 UI 時、用 CSS variable 傳遞：</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="p">:</span><span class="nd">root</span> <span class="p">{</span> <span class="nv">--custom-height</span><span class="p">:</span> <span class="mi">60</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">framework-element</span> <span class="p">{</span> <span class="k">margin-top</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">custom</span><span class="o">-</span><span class="n">height</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">custom-ui</span> <span class="p">{</span> <span class="k">height</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">custom</span><span class="o">-</span><span class="n">height</span><span class="p">);</span> <span class="p">}</span></span></span></code></pre></div><p>或用 ResizeObserver 量測寫回 variable（<a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a>）。</p>
<hr>
<h2 id="設計取捨客製-ui-的位置選擇">設計取捨：客製 UI 的位置選擇</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（邊界外 + CSS）當預設、其他做法在特定情境合理。</p>
<h3 id="aframework-邊界外--css-視覺定位這個專案的預設">A：framework 邊界外 + CSS 視覺定位（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：客製 UI 放在 framework 元件的 sibling 位置、用 CSS absolute / grid / margin spacer 達成視覺位置</li>
<li><strong>選 A 的理由</strong>：跟 framework reconciliation 完全解耦、命運由自己決定、升級不影響</li>
<li><strong>適合</strong>：絕大多數需要在 framework UI 旁 / 上 / 下加客製內容的情境</li>
<li><strong>代價</strong>：CSS 定位邏輯比 DOM 巢狀複雜、需要正確處理 stacking context / z-index</li>
</ul>
<h3 id="bframework-邊界外--js-量測位置">B：framework 邊界外 + JS 量測位置</h3>
<ul>
<li><strong>機制</strong>：用 ResizeObserver 量 framework 元素的 bounding rect、JS 算出客製 UI 該擺哪</li>
<li><strong>跟 A 的取捨</strong>：A 用 CSS 表達靜態關係、B 處理 runtime 才知道的尺寸；B 多一層 JS、但能達成 CSS 表達不出的精確定位</li>
<li><strong>B 比 A 好的情境</strong>：客製 UI 位置依賴 framework 元件的 runtime 尺寸（內容換行、字型變化）</li>
</ul>
<h3 id="cframework-邊界內注入">C：framework 邊界內注入</h3>
<ul>
<li><strong>機制</strong>：JS 把客製 element 直接 appendChild 到 framework 子樹內</li>
<li><strong>跟 A 的取捨</strong>：C 看似省事（少一層 wrapper）、實際把客製命運綁在 framework reconciliation 上</li>
<li><strong>C 才合理的情境</strong>：該 framework 子樹確認「不會被 reconcile」（極罕見、需要讀框架 source 確認）</li>
<li><strong>代價</strong>：客製可能在任何 patch 時消失、需要 MutationObserver 補打、跟渲染週期賽跑</li>
</ul>
<h3 id="dfork-framework-source">D：Fork framework source</h3>
<ul>
<li><strong>機制</strong>：fork 整個 framework、改 reconciliation 行為讓它認得我們的客製</li>
<li><strong>成本特別高的原因</strong>：每次升級都要重新 merge、客製永久綁在 fork 版本</li>
<li><strong>D 才合理的情境</strong>：framework 已停止維護、且客製需求超過所有其他選項</li>
</ul>
<hr>
<h2 id="不該套用邊界外的情境">不該套用「邊界外」的情境</h2>
<p>A 是預設、但不是萬靈丹：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不適合 A</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客製內容必須在 framework 元件的內部視覺脈絡內（共享 inline flow）</td>
          <td>Absolute 跳出 flow、達不到 inline 的視覺效果</td>
      </tr>
      <tr>
          <td>Framework 元件本身就是要客製化（改 row、改 cell）</td>
          <td>動的是 framework 本身、不是「在旁邊加東西」</td>
      </tr>
      <tr>
          <td>Framework 提供了官方擴展介面（slot、render prop）</td>
          <td>用官方介面更穩、不需要邊界外 hack</td>
      </tr>
      <tr>
          <td>客製需要訪問 framework 的內部 state</td>
          <td>邊界外的客製跟內部 state 隔離、訪問成本高</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：客製是「在 framework 旁邊加東西」還是「改 framework 本身」？前者用本策略、後者另想辦法。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>抽象層原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a></td>
          <td>「邊界外 + CSS」是「不要挖 framework 內部」的具體應用 — 客製貼著外部介面（DOM sibling）做、不挖內部</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>第 1 次注入失敗（被清掉）= 第 2 次該換策略到邊界外、不該繼續嘗試「換種方式注入」</td>
      </tr>
      <tr>
          <td><a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a></td>
          <td>互補關係 — 本篇處理「客製 UI 該放哪」、#13 處理「framework 元件本身要動時怎麼動」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該怎麼處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>注入 framework DOM 的元素在使用者互動後消失</td>
          <td>把該元素搬出 framework 邊界、用 CSS 控制視覺位置</td>
      </tr>
      <tr>
          <td>客製 UI 在 framework 更新後 attribute 被 revert</td>
          <td>客製 UI 不該在 framework 內、wrapper 在外、attribute 套 wrapper</td>
      </tr>
      <tr>
          <td>看不出哪些 DOM 是 framework 管的</td>
          <td>讀 framework 的 mount root、從那裡往內都是管轄區</td>
      </tr>
      <tr>
          <td>Stacking context 衝突、z-index 失靈</td>
          <td>確認 absolute 的 containing block 是預期的 relative parent</td>
      </tr>
      <tr>
          <td>Framework 元件位置不固定、客製 UI 對不齊</td>
          <td>用 ResizeObserver 量 framework 元素、寫回 CSS variable</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：客製 UI 的存活壽命 = 「離 framework 管轄區多遠」。最遠 = 永遠不被清；注入內部 = 隨時可能消失。預設選邊界外、不要為了「省一層 wrapper」進入 framework 領地。</p>
]]></content:encoded></item><item><title>Filter 順序由使用者掃描成本決定</title><link>https://tarrragon.github.io/blog/report/filter-order-by-scan-cost/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/filter-order-by-scan-cost/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>選項清單按使用者掃描順序排序、不按資料來源預設。&lt;/strong> 短清單先看完、長清單花更多時間 — 把短清單放前面讓使用者先排除一個維度、再面對長清單。字母排序對「找已知名稱」有效；多選 facet 場景下使用者通常不知道確切選項名、需要 scan，這時候掃描成本主導。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼掃描成本優先於字母排序">為什麼掃描成本優先於字母排序&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>當清單有多個選項供使用者挑選，&lt;strong>選項數量影響掃描時間&lt;/strong>。使用者在 facet UI 的行為不是「找已知 tag」、是「看到有什麼可選、選有興趣的」 — 這是探索式行為、不是查找式行為。&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>字母排序 — 二分查找&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>探索式（看有什麼）&lt;/td>
 &lt;td>掃描成本排序 — 短清單先&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>字母排序是資料庫思維（穩定、可預測），掃描成本排序是 UX 思維（縮短決策時間）。Facet 場景幾乎都是後者。&lt;/p>
&lt;h3 id="兩條維度收斂的順序">兩條維度收斂的順序&lt;/h3>
&lt;p>當有多個 facet 維度可選、使用者通常&lt;strong>逐維度收斂&lt;/strong>：先用一個維度砍掉一半結果、再用第二個維度精選。短清單放前面讓「第一刀」決策快速。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的應用">這次任務的應用&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>Pagefind filter 預設按 filter key 字母排序：&lt;code>tag&lt;/code> &amp;lt; &lt;code>type&lt;/code>，所以 Tag 先顯示。&lt;/p>
&lt;p>實際內容：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Filter&lt;/th>
 &lt;th>選項數量&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Type&lt;/td>
 &lt;td>~5 個（post / card / glossary 等 section）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tag&lt;/td>
 &lt;td>~80 個（站上所有 tags）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>Type 短、Tag 長 — 預期使用者行為是「先按 type 收斂（這是 post 還是 glossary？）、再進 tag 找主題」。Tag 在前等於要使用者先面對 80 個選項，認知成本高。&lt;/p>
&lt;p>把順序倒過來：Type 先顯示讓使用者先用 section 收斂、再進 Tag 找。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">reorderFilters&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">blocks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">filter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-block&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">desiredOrder&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;tag&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">byKey&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">blocks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">b&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-name&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">textContent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">trim&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">toLowerCase&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">desiredOrder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">k&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">filter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">]));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;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;/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>探索式 facet&lt;/td>
 &lt;td>「短」「長」邊界模糊時不穩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用頻率排序&lt;/td>
 &lt;td>有 analytics 資料&lt;/td>
 &lt;td>冷啟動時無資料、需要默認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語意分組排序&lt;/td>
 &lt;td>選項有明顯子類別&lt;/td>
 &lt;td>子類別劃分本身需要設計&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇順序：&lt;strong>有 analytics → 頻率排序；沒有 → 掃描成本排序；都不適用 → 字母排序&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="排序原則的延伸應用">排序原則的延伸應用&lt;/h2>
&lt;h3 id="同類-facet-內的選項排序">同類 facet 內的選項排序&lt;/h3>
&lt;p>Tag 內部的 80 個 tag 怎麼排？&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>使用者知道想找哪個 tag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>計數排序（最多文章的 tag 在前）&lt;/td>
 &lt;td>探索式、引導使用者進熱門主題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編輯精選&lt;/td>
 &lt;td>站方有特定主題策略&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>當前接受 pagefind 的字母排序（成本最低、且 80 個 tag 做計數排序需要額外索引處理）。&lt;/p>
&lt;h3 id="選項數量門檻">選項數量門檻&lt;/h3>
&lt;p>短清單跟長清單的邊界沒有絕對值，常用啟發：&lt;/p>
&lt;ul>
&lt;li>≤ 7 個選項：可一眼掃完、放前面&lt;/li>
&lt;li>8-20 個：中等、需要結構（縮排、分組）&lt;/li>
&lt;li>20+ 個：長清單、放後面或加搜尋框&lt;/li>
&lt;/ul>
&lt;p>Type 5 個落在「短」、Tag 80 個落在「長」 — 順序明顯。&lt;/p>
&lt;hr>
&lt;h2 id="設計取捨選項清單的排序策略">設計取捨：選項清單的排序策略&lt;/h2>
&lt;p>四種做法、各自機會成本不同。預設依使用者行為性質選 — 探索式 → A、查找式 → B、有 analytics → C、有子類別 → D。&lt;/p>
&lt;h3 id="a掃描成本排序短先長後探索式-facet-的預設">A：掃描成本排序（短先長後）（探索式 facet 的預設）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：選項數量少的維度放前面（type 5 個 → 先；tag 80 個 → 後）&lt;/li>
&lt;li>&lt;strong>選 A 的理由&lt;/strong>：使用者先用短清單收斂一刀、再面對長清單；認知成本低&lt;/li>
&lt;li>&lt;strong>適合&lt;/strong>：探索式 facet（使用者不知道確切選項名）&lt;/li>
&lt;li>&lt;strong>代價&lt;/strong>：需要主動覆寫資料來源預設（不能直接用 DB 順序）&lt;/li>
&lt;/ul>
&lt;h3 id="b字母排序">B：字母排序&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：按 alphabetical 排&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：B 對「找已知名稱」高效（二分查找）、A 對「探索式選擇」高效；但 facet 場景幾乎都是探索式&lt;/li>
&lt;li>&lt;strong>B 比 A 好的情境&lt;/strong>：使用者通常知道確切選項名（國家清單、語言清單）&lt;/li>
&lt;/ul>
&lt;h3 id="c使用頻率排序最常用在前">C：使用頻率排序（最常用在前）&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：按 analytics 統計、最高頻選項在前&lt;/li>
&lt;li>&lt;strong>跟 A/B 的取捨&lt;/strong>：C 比 A 更精準（用真實資料）、但需要 analytics + 冷啟動時無資料&lt;/li>
&lt;li>&lt;strong>C 比 A 好的情境&lt;/strong>：有足夠 analytics、且使用者偏好集中（80/20 分布明顯）&lt;/li>
&lt;/ul>
&lt;h3 id="d語意分組排序">D：語意分組排序&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>機制&lt;/strong>：把選項分子類別、組內再排&lt;/li>
&lt;li>&lt;strong>跟 A 的取捨&lt;/strong>：D 對「選項有明顯子類別」更直觀（產品類別 / 功能類別）、A 對純扁平清單夠用&lt;/li>
&lt;li>&lt;strong>D 比 A 好的情境&lt;/strong>：選項有清楚的層級結構（電商 facet：類別 &amp;gt; 子類別 &amp;gt; 選項）&lt;/li>
&lt;/ul>
&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;/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>資料來源排序與 UX 不符&lt;/td>
 &lt;td>主動 reorder、不接受預設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>新增選項後順序錯亂&lt;/td>
 &lt;td>reorder 邏輯依賴 hardcoded list&lt;/td>
 &lt;td>改用屬性分類（例如選項數量自動排）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：UI 排序是設計決策、不是技術選擇。預設順序通常反映資料來源結構、不反映使用者行為 — 主動覆寫是常態、不是例外。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>選項清單按使用者掃描順序排序、不按資料來源預設。</strong> 短清單先看完、長清單花更多時間 — 把短清單放前面讓使用者先排除一個維度、再面對長清單。字母排序對「找已知名稱」有效；多選 facet 場景下使用者通常不知道確切選項名、需要 scan，這時候掃描成本主導。</p>
<hr>
<h2 id="為什麼掃描成本優先於字母排序">為什麼掃描成本優先於字母排序</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>當清單有多個選項供使用者挑選，<strong>選項數量影響掃描時間</strong>。使用者在 facet UI 的行為不是「找已知 tag」、是「看到有什麼可選、選有興趣的」 — 這是探索式行為、不是查找式行為。</p>
<table>
  <thead>
      <tr>
          <th>行為類型</th>
          <th>適合的排序</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>查找式（知道要什麼）</td>
          <td>字母排序 — 二分查找</td>
      </tr>
      <tr>
          <td>探索式（看有什麼）</td>
          <td>掃描成本排序 — 短清單先</td>
      </tr>
  </tbody>
</table>
<p>字母排序是資料庫思維（穩定、可預測），掃描成本排序是 UX 思維（縮短決策時間）。Facet 場景幾乎都是後者。</p>
<h3 id="兩條維度收斂的順序">兩條維度收斂的順序</h3>
<p>當有多個 facet 維度可選、使用者通常<strong>逐維度收斂</strong>：先用一個維度砍掉一半結果、再用第二個維度精選。短清單放前面讓「第一刀」決策快速。</p>
<hr>
<h2 id="這次任務的應用">這次任務的應用</h2>
<h3 id="觀察">觀察</h3>
<p>Pagefind filter 預設按 filter key 字母排序：<code>tag</code> &lt; <code>type</code>，所以 Tag 先顯示。</p>
<p>實際內容：</p>
<table>
  <thead>
      <tr>
          <th>Filter</th>
          <th>選項數量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type</td>
          <td>~5 個（post / card / glossary 等 section）</td>
      </tr>
      <tr>
          <td>Tag</td>
          <td>~80 個（站上所有 tags）</td>
      </tr>
  </tbody>
</table>
<h3 id="判讀">判讀</h3>
<p>Type 短、Tag 長 — 預期使用者行為是「先按 type 收斂（這是 post 還是 glossary？）、再進 tag 找主題」。Tag 在前等於要使用者先面對 80 個選項，認知成本高。</p>
<p>把順序倒過來：Type 先顯示讓使用者先用 section 收斂、再進 Tag 找。</p>
<h3 id="執行">執行</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">reorderFilters</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">blocks</span> <span class="o">=</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-block&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">desiredOrder</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">,</span> <span class="s1">&#39;tag&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">var</span> <span class="nx">byKey</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">blocks</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">b</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-name&#39;</span><span class="p">).</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">trim</span><span class="p">().</span><span class="nx">toLowerCase</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">byKey</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">b</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">desiredOrder</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">k</span> <span class="p">=&gt;</span> <span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">]</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">]));</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><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>掃描成本排序（短先長後）</td>
          <td>探索式 facet</td>
          <td>「短」「長」邊界模糊時不穩</td>
      </tr>
      <tr>
          <td>使用頻率排序</td>
          <td>有 analytics 資料</td>
          <td>冷啟動時無資料、需要默認</td>
      </tr>
      <tr>
          <td>語意分組排序</td>
          <td>選項有明顯子類別</td>
          <td>子類別劃分本身需要設計</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>有 analytics → 頻率排序；沒有 → 掃描成本排序；都不適用 → 字母排序</strong>。</p>
<hr>
<h2 id="排序原則的延伸應用">排序原則的延伸應用</h2>
<h3 id="同類-facet-內的選項排序">同類 facet 內的選項排序</h3>
<p>Tag 內部的 80 個 tag 怎麼排？</p>
<table>
  <thead>
      <tr>
          <th>內部排序方式</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>字母排序</td>
          <td>使用者知道想找哪個 tag</td>
      </tr>
      <tr>
          <td>計數排序（最多文章的 tag 在前）</td>
          <td>探索式、引導使用者進熱門主題</td>
      </tr>
      <tr>
          <td>編輯精選</td>
          <td>站方有特定主題策略</td>
      </tr>
  </tbody>
</table>
<p>當前接受 pagefind 的字母排序（成本最低、且 80 個 tag 做計數排序需要額外索引處理）。</p>
<h3 id="選項數量門檻">選項數量門檻</h3>
<p>短清單跟長清單的邊界沒有絕對值，常用啟發：</p>
<ul>
<li>≤ 7 個選項：可一眼掃完、放前面</li>
<li>8-20 個：中等、需要結構（縮排、分組）</li>
<li>20+ 個：長清單、放後面或加搜尋框</li>
</ul>
<p>Type 5 個落在「短」、Tag 80 個落在「長」 — 順序明顯。</p>
<hr>
<h2 id="設計取捨選項清單的排序策略">設計取捨：選項清單的排序策略</h2>
<p>四種做法、各自機會成本不同。預設依使用者行為性質選 — 探索式 → A、查找式 → B、有 analytics → C、有子類別 → D。</p>
<h3 id="a掃描成本排序短先長後探索式-facet-的預設">A：掃描成本排序（短先長後）（探索式 facet 的預設）</h3>
<ul>
<li><strong>機制</strong>：選項數量少的維度放前面（type 5 個 → 先；tag 80 個 → 後）</li>
<li><strong>選 A 的理由</strong>：使用者先用短清單收斂一刀、再面對長清單；認知成本低</li>
<li><strong>適合</strong>：探索式 facet（使用者不知道確切選項名）</li>
<li><strong>代價</strong>：需要主動覆寫資料來源預設（不能直接用 DB 順序）</li>
</ul>
<h3 id="b字母排序">B：字母排序</h3>
<ul>
<li><strong>機制</strong>：按 alphabetical 排</li>
<li><strong>跟 A 的取捨</strong>：B 對「找已知名稱」高效（二分查找）、A 對「探索式選擇」高效；但 facet 場景幾乎都是探索式</li>
<li><strong>B 比 A 好的情境</strong>：使用者通常知道確切選項名（國家清單、語言清單）</li>
</ul>
<h3 id="c使用頻率排序最常用在前">C：使用頻率排序（最常用在前）</h3>
<ul>
<li><strong>機制</strong>：按 analytics 統計、最高頻選項在前</li>
<li><strong>跟 A/B 的取捨</strong>：C 比 A 更精準（用真實資料）、但需要 analytics + 冷啟動時無資料</li>
<li><strong>C 比 A 好的情境</strong>：有足夠 analytics、且使用者偏好集中（80/20 分布明顯）</li>
</ul>
<h3 id="d語意分組排序">D：語意分組排序</h3>
<ul>
<li><strong>機制</strong>：把選項分子類別、組內再排</li>
<li><strong>跟 A 的取捨</strong>：D 對「選項有明顯子類別」更直觀（產品類別 / 功能類別）、A 對純扁平清單夠用</li>
<li><strong>D 比 A 好的情境</strong>：選項有清楚的層級結構（電商 facet：類別 &gt; 子類別 &gt; 選項）</li>
</ul>
<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>同類選項使用者卡在第一步</td>
          <td>第一個維度選項過多</td>
          <td>換成更少選項的維度當第一步</td>
      </tr>
      <tr>
          <td>預設排序看起來「隨機」</td>
          <td>資料來源排序與 UX 不符</td>
          <td>主動 reorder、不接受預設</td>
      </tr>
      <tr>
          <td>新增選項後順序錯亂</td>
          <td>reorder 邏輯依賴 hardcoded list</td>
          <td>改用屬性分類（例如選項數量自動排）</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：UI 排序是設計決策、不是技術選擇。預設順序通常反映資料來源結構、不反映使用者行為 — 主動覆寫是常態、不是例外。</p>
<p>跟 <a href="../view-layer-filter-vs-source-layer/">#55-#66 Filter × Source 系列</a> 的關係：本卡是「filter UI 的排序」、Filter × Source 系列是「filter 行為的層級」 — 兩個維度互補。設計 filter UI 時兩者都要顧：本卡決定「哪個選項放前面」、<a href="../filter-instruction-clarification/">#58</a> 決定「篩選的定義域是哪一層」、<a href="../filter-source-composition-strategies/">#59</a> 決定「filter 跟 source 怎麼合成」。</p>
]]></content:encoded></item><item><title>量測值缺一不可：依賴未測量值會錯位</title><link>https://tarrragon.github.io/blog/report/measurement-completeness/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/measurement-completeness/</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>對齊不是「視覺感」、是「相對位置的數學關係」。filter 的 padding-top 要等於右側「H1 + input + gap」的總和；任何一個值不準、padding 就錯、視覺上看起來就是沒對齊。&lt;/p>
&lt;p>人眼可以分辨 1px 的差異 — 估算「大概 60px」實際上 56 或 64 都可能、視覺一眼看出。&lt;/p>
&lt;h3 id="解線性方程組需要所有變數">解線性方程組需要所有變數&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>步驟&lt;/th>
 &lt;th>動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>列出對齊基準上的所有元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>對每個元素標註「值的來源」：寫死 / 量測 / 未知&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>任何「未知」都要先解決（決定寫死或量測）才能寫對齊規則&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跳過第 2 步直接寫對齊規則 = 拿一組有未知數的方程組嘗試代入解 — 不會對。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把 filter sidebar 的內容上緣對齊到右側 results 上緣。filter 用 &lt;code>padding-top&lt;/code> 把內容下推。&lt;/p>
&lt;p>第一次嘗試：估 &lt;code>padding-top: 152px&lt;/code>（H1 64 + input 68 + gap 20）。&lt;/p>
&lt;p>實際渲染：filter 上緣比 results 上緣高了 ~10px。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>&lt;code>152px&lt;/code> 的計算用了估算的 H1 height（64px）。實際 H1 受 theme 的 &lt;code>margin-block-end&lt;/code> 影響、總高度可能 ~70px。差了 ~6px。&lt;/p>
&lt;p>進一步檢查：&lt;code>--pagefind-ui-scale: 0.8&lt;/code> 時 input 高度 = 64 × 0.8 = 51.2px、不是 68px。又差 ~17px。&lt;/p>
&lt;p>差距加總超過視覺可接受範圍。&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>H1&lt;/td>
 &lt;td>寫死 height + line-height + margin: 0，強制等於 token&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pagefind input&lt;/td>
 &lt;td>設 &lt;code>--pagefind-ui-scale: 1.0&lt;/code>，加 border 共 68px、強制等於 token&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope UI（高度受字型換行影響）&lt;/td>
 &lt;td>用 ResizeObserver 量測寫回 CSS 變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Gap（drawer margin-top）&lt;/td>
 &lt;td>從 pagefind CSS 取得固定值 20px&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>padding-top&lt;/code> 用 &lt;code>calc()&lt;/code> 加總所有變數、永遠跟著走。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Design token（CSS 變數寫死）&lt;/td>
 &lt;td>設計可決定的固定值&lt;/td>
 &lt;td>低 — 改一處全部跟上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>組件提供的 hook（如 pagefind scale）&lt;/td>
 &lt;td>透過組件 API 鎖定渲染參數&lt;/td>
 &lt;td>低 — 跟組件升級走&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime 量測（ResizeObserver）&lt;/td>
 &lt;td>內容動態決定的值&lt;/td>
 &lt;td>中 — JS 程式要寫對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>估算 / magic number&lt;/td>
 &lt;td>不適用 — 永遠錯&lt;/td>
 &lt;td>不該存在&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>不要把「估算 / magic number」當作來源&lt;/strong>。每個 magic number 都是未來 debug 的潛在炸彈。&lt;/p>
&lt;hr>
&lt;h2 id="把對齊看成方程組的步驟">把對齊看成方程組的步驟&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">基準線 P 的位置 = sum(每個前置元素的 height + margin + padding + gap)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例：filter 的 &lt;code>padding-top&lt;/code> = &lt;code>H1.height + input.height + drawer.margin-top&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>對齊基準上的每個未知數都要解出來、整組才有解。</strong> 這跟線性方程組一樣 — 任何一個變數靠估算、整條基準線就不準。每個參與對齊的元素都需要「來源明確的數字」（寫死或量測），不能依賴「應該差不多吧」的視覺直覺。</p>
<hr>
<h2 id="為什麼缺一個值整個壞">為什麼缺一個值整個壞</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>對齊不是「視覺感」、是「相對位置的數學關係」。filter 的 padding-top 要等於右側「H1 + input + gap」的總和；任何一個值不準、padding 就錯、視覺上看起來就是沒對齊。</p>
<p>人眼可以分辨 1px 的差異 — 估算「大概 60px」實際上 56 或 64 都可能、視覺一眼看出。</p>
<h3 id="解線性方程組需要所有變數">解線性方程組需要所有變數</h3>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>列出對齊基準上的所有元素</td>
      </tr>
      <tr>
          <td>2</td>
          <td>對每個元素標註「值的來源」：寫死 / 量測 / 未知</td>
      </tr>
      <tr>
          <td>3</td>
          <td>任何「未知」都要先解決（決定寫死或量測）才能寫對齊規則</td>
      </tr>
  </tbody>
</table>
<p>跳過第 2 步直接寫對齊規則 = 拿一組有未知數的方程組嘗試代入解 — 不會對。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>要把 filter sidebar 的內容上緣對齊到右側 results 上緣。filter 用 <code>padding-top</code> 把內容下推。</p>
<p>第一次嘗試：估 <code>padding-top: 152px</code>（H1 64 + input 68 + gap 20）。</p>
<p>實際渲染：filter 上緣比 results 上緣高了 ~10px。</p>
<h3 id="判讀">判讀</h3>
<p><code>152px</code> 的計算用了估算的 H1 height（64px）。實際 H1 受 theme 的 <code>margin-block-end</code> 影響、總高度可能 ~70px。差了 ~6px。</p>
<p>進一步檢查：<code>--pagefind-ui-scale: 0.8</code> 時 input 高度 = 64 × 0.8 = 51.2px、不是 68px。又差 ~17px。</p>
<p>差距加總超過視覺可接受範圍。</p>
<h3 id="執行">執行</h3>
<p>把所有變數轉為「來源明確」：</p>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>解決方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>H1</td>
          <td>寫死 height + line-height + margin: 0，強制等於 token</td>
      </tr>
      <tr>
          <td>Pagefind input</td>
          <td>設 <code>--pagefind-ui-scale: 1.0</code>，加 border 共 68px、強制等於 token</td>
      </tr>
      <tr>
          <td>Scope UI（高度受字型換行影響）</td>
          <td>用 ResizeObserver 量測寫回 CSS 變數</td>
      </tr>
      <tr>
          <td>Gap（drawer margin-top）</td>
          <td>從 pagefind CSS 取得固定值 20px</td>
      </tr>
  </tbody>
</table>
<p><code>padding-top</code> 用 <code>calc()</code> 加總所有變數、永遠跟著走。</p>
<hr>
<h2 id="內在屬性比較值的來源分類">內在屬性比較：值的「來源」分類</h2>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>適用情境</th>
          <th>維護負擔</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Design token（CSS 變數寫死）</td>
          <td>設計可決定的固定值</td>
          <td>低 — 改一處全部跟上</td>
      </tr>
      <tr>
          <td>組件提供的 hook（如 pagefind scale）</td>
          <td>透過組件 API 鎖定渲染參數</td>
          <td>低 — 跟組件升級走</td>
      </tr>
      <tr>
          <td>Runtime 量測（ResizeObserver）</td>
          <td>內容動態決定的值</td>
          <td>中 — JS 程式要寫對</td>
      </tr>
      <tr>
          <td>估算 / magic number</td>
          <td>不適用 — 永遠錯</td>
          <td>不該存在</td>
      </tr>
  </tbody>
</table>
<p><strong>不要把「估算 / magic number」當作來源</strong>。每個 magic number 都是未來 debug 的潛在炸彈。</p>
<hr>
<h2 id="把對齊看成方程組的步驟">把對齊看成方程組的步驟</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">基準線 P 的位置 = sum(每個前置元素的 height + margin + padding + gap)</span></span></code></pre></div><p>例：filter 的 <code>padding-top</code> = <code>H1.height + input.height + drawer.margin-top</code>。</p>
<p>把每個變數列出、確認來源、用 CSS <code>calc()</code> + 變數寫成 single source。</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="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">padding-top</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <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="c">/* 寫死 64px */</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <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="c">/* 鎖 scale=1.0、寫死 68px */</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <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">scope</span><span class="o">-</span><span class="n">h</span><span class="p">)</span>      <span class="c">/* ResizeObserver 量測 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="o">+</span> <span class="mi">8</span><span class="kt">px</span>                        <span class="c">/* 固定 padding */</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <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 class="c">/* pagefind drawer margin 20px */</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="設計取捨對齊基準上每個值的來源">設計取捨：對齊基準上每個值的來源</h2>
<p>四種做法、各自機會成本不同。預設選擇取決於值的可預測性 — 設計可決定 → A、組件提供 hook → B、內容動態 → C、估算永遠不是答案。</p>
<h3 id="adesign-tokencss-變數寫死">A：Design token（CSS 變數寫死）</h3>
<ul>
<li><strong>機制</strong>：<code>--search-title-h: 64px</code> 寫成設計系統 token</li>
<li><strong>選 A 的理由</strong>：build time 確定、純 CSS、改 token 全部跟上</li>
<li><strong>適合</strong>：設計可決定的固定值（spacing、typography scale、icon size）</li>
<li><strong>代價</strong>：值無法跟 runtime 內容變動 — 字型大幅變化時可能不適配</li>
</ul>
<h3 id="b組件提供的-hook如-pagefind-scale">B：組件提供的 hook（如 pagefind scale）</h3>
<ul>
<li><strong>機制</strong>：<code>.search-shell { --pagefind-ui-scale: 1.0 }</code>、透過組件 API 鎖定渲染參數</li>
<li><strong>跟 A 的取捨</strong>：B 把組件納入自家設計系統、A 自己決定值；B 在「組件渲染參數可調」時最乾淨</li>
<li><strong>B 比 A 好的情境</strong>：值由組件決定但組件提供 hook 可控（例如 vendor library 的 size variant）</li>
</ul>
<h3 id="cruntime-量測resizeobserver-寫回-css-變數">C：Runtime 量測（ResizeObserver 寫回 CSS 變數）</h3>
<ul>
<li><strong>機制</strong>：JS 量元素實際渲染尺寸、寫回 CSS 變數、其他元素引用</li>
<li><strong>跟 A/B 的取捨</strong>：C 自動跟著實際走、A/B 假設條件穩定；C 多 JS 一層</li>
<li><strong>C 比 A 好的情境</strong>：值受字型 / 換行 / 內容動態影響、無法 build time 預測</li>
</ul>
<h3 id="d估算--magic-number">D：估算 / magic number</h3>
<ul>
<li><strong>機制</strong>：執行者依感覺給數字、不寫變數、不量測</li>
<li><strong>成本特別高的原因</strong>：未來 debug 的潛在炸彈、估錯時錯誤被視覺接受不被發現、跨情境（字型 / scale / theme）必壞</li>
<li><strong>D 是反模式</strong>：任何「沒來源」的值都是 unsolved 變數、會在邊界情境爆掉</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視覺對齊「看起來差幾 px」</td>
          <td>某個元素的高度估算不準</td>
          <td>量測該元素真實 height、跟假設值比對</td>
      </tr>
      <tr>
          <td>換 viewport 對齊壞掉</td>
          <td>某個值依賴 viewport 但沒處理</td>
          <td>找出該值、改用響應式變數</td>
      </tr>
      <tr>
          <td>換字型 / 縮放後對齊壞掉</td>
          <td>某個值受字型影響但寫死了</td>
          <td>改用 ResizeObserver 量測</td>
      </tr>
      <tr>
          <td>改某個 token 要去多處跟改</td>
          <td>沒用 CSS 變數</td>
          <td>把 magic number 提成變數、calc 串起來</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：對齊問題的根因永遠是「某個變數沒解出來」。先找出那個變數、確定來源、再寫對齊規則。</p>
<p>「估值補方程式」是便利（用感覺寫死）、「找變數真實來源」是對齊（量測或從 token 算）— 同 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。對齊問題的 silent 失敗常常是估值的後果。</p>
]]></content:encoded></item><item><title>置中元件與絕對定位元件並存：用疊層而非排擠</title><link>https://tarrragon.github.io/blog/report/centered-and-positioned-coexistence/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/centered-and-positioned-coexistence/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>置中元件與指定位置元件共存，正確做法是讓兩者位於不同 layout 層。&lt;/strong> Layout 流負責「以內容驅動的尺寸與置中」；絕對定位負責「貼在 layout 流之上的固定位置元件」。兩者用疊層共存、不互相排擠。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼排擠式做法行不通">為什麼排擠式做法行不通&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>排擠式：把兩個元件放進同一個 grid / flex container、各佔一個欄位。問題在於「容器寬度有限、加一個欄位就壓縮另一個」 — 中央欄想置中時，加 sidebar 後整個 layout 重新分佈、中央欄被推到非置中位置。&lt;/p>
&lt;p>疊層式：sidebar 用 &lt;code>position: absolute&lt;/code> 從 layout 流跳出 — 中央欄看不到 sidebar、繼續按自己的規則置中。&lt;/p>
&lt;h3 id="兩種共存模式比較">兩種共存模式比較&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模式&lt;/th>
 &lt;th>中央欄置中&lt;/th>
 &lt;th>維護成本&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>排擠式（同 layout 流）&lt;/td>
 &lt;td>受 sidebar 影響、需要重算&lt;/td>
 &lt;td>低 — 純 CSS&lt;/td>
 &lt;td>兩個元件都要自然撐開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>疊層式（absolute）&lt;/td>
 &lt;td>不受影響、永遠按 viewport 置中&lt;/td>
 &lt;td>中 — absolute 需要定位基準&lt;/td>
 &lt;td>中央欄要嚴格置中、sidebar 是「附加」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇疊層式的時機：&lt;strong>中央欄的位置是設計重點、sidebar 是補充&lt;/strong>。本案搜尋頁的 main 是內容主體、filter 是輔助 — 適合疊層。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際做法">這次任務的實際做法&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁 desktop layout：&lt;/p>
&lt;ul>
&lt;li>&lt;code>&amp;lt;main&amp;gt;&lt;/code> 寬度 70ch、theme 預設 &lt;code>margin-inline: auto&lt;/code> 置中&lt;/li>
&lt;li>想加 filter sidebar 在 main 左外側、寬度 400px&lt;/li>
&lt;/ul>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 filter 放進 main 的 grid（變成 main 的子 column），main 內容會被推到右半邊、不再置中。&lt;/p>
&lt;p>讓 filter 用 &lt;code>position: absolute&lt;/code> 相對 main 定位 — main 完全不知道 filter 存在、繼續置中。filter 在 main 外側「貼著」main 的左邊緣。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>





&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="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&lt;/span> &lt;span class="nt">main&lt;/span>&lt;span class="p">#&lt;/span>&lt;span class="nn">main-content&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">relative&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* filter 的 offset parent */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="c">/* main 的 max-width: 70ch 與 margin-inline: auto 由 theme 提供，不動 */&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>&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 class="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">top&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">right&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">calc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="kt">%&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="kt">rem&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c">/* main 的左外側、間距 2rem */&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">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="kt">px&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="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>right: calc(100% + 2rem)&lt;/code> 的含義：filter 的右邊緣 = main 左邊緣 - 2rem。filter 從這個 anchor 往左展開 400px。&lt;/p>
&lt;p>main 永遠按 viewport 置中、filter 永遠貼在 main 左外側。&lt;/p>
&lt;hr>
&lt;h2 id="疊層共存的三個關鍵要素">疊層共存的三個關鍵要素&lt;/h2>
&lt;h3 id="1-offset-parent-的選擇">1. Offset parent 的選擇&lt;/h3>
&lt;p>絕對定位元件的座標相對於「最近的 positioned ancestor」。要讓 sidebar 跟著 main 一起移動（不要跟 viewport 走），就把 main 設為 &lt;code>position: relative&lt;/code> 當作 offset parent。&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="nt">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">page-search&lt;/span> &lt;span class="nt">main&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">relative&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">2&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 相對 main */&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-anchor-點的選擇">2. Anchor 點的選擇&lt;/h3>
&lt;p>&lt;code>right: calc(100% + 2rem)&lt;/code>、&lt;code>left: -432px&lt;/code>、&lt;code>right: 100%; margin-right: 2rem&lt;/code> 三種寫法視覺上等價。選擇可讀性最高的 — 通常是 &lt;code>right: calc(100% + 2rem)&lt;/code>，意義最直接（「我的右緣 = parent 寬度 + 2rem 之外」）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>置中元件與指定位置元件共存，正確做法是讓兩者位於不同 layout 層。</strong> Layout 流負責「以內容驅動的尺寸與置中」；絕對定位負責「貼在 layout 流之上的固定位置元件」。兩者用疊層共存、不互相排擠。</p>
<hr>
<h2 id="為什麼排擠式做法行不通">為什麼排擠式做法行不通</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>排擠式：把兩個元件放進同一個 grid / flex container、各佔一個欄位。問題在於「容器寬度有限、加一個欄位就壓縮另一個」 — 中央欄想置中時，加 sidebar 後整個 layout 重新分佈、中央欄被推到非置中位置。</p>
<p>疊層式：sidebar 用 <code>position: absolute</code> 從 layout 流跳出 — 中央欄看不到 sidebar、繼續按自己的規則置中。</p>
<h3 id="兩種共存模式比較">兩種共存模式比較</h3>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>中央欄置中</th>
          <th>維護成本</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>排擠式（同 layout 流）</td>
          <td>受 sidebar 影響、需要重算</td>
          <td>低 — 純 CSS</td>
          <td>兩個元件都要自然撐開</td>
      </tr>
      <tr>
          <td>疊層式（absolute）</td>
          <td>不受影響、永遠按 viewport 置中</td>
          <td>中 — absolute 需要定位基準</td>
          <td>中央欄要嚴格置中、sidebar 是「附加」</td>
      </tr>
  </tbody>
</table>
<p>選擇疊層式的時機：<strong>中央欄的位置是設計重點、sidebar 是補充</strong>。本案搜尋頁的 main 是內容主體、filter 是輔助 — 適合疊層。</p>
<hr>
<h2 id="這次任務的實際做法">這次任務的實際做法</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁 desktop layout：</p>
<ul>
<li><code>&lt;main&gt;</code> 寬度 70ch、theme 預設 <code>margin-inline: auto</code> 置中</li>
<li>想加 filter sidebar 在 main 左外側、寬度 400px</li>
</ul>
<h3 id="判讀">判讀</h3>
<p>把 filter 放進 main 的 grid（變成 main 的子 column），main 內容會被推到右半邊、不再置中。</p>
<p>讓 filter 用 <code>position: absolute</code> 相對 main 定位 — main 完全不知道 filter 存在、繼續置中。filter 在 main 外側「貼著」main 的左邊緣。</p>
<h3 id="執行">執行</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="nt">main</span><span class="p">#</span><span class="nn">main-content</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">relative</span><span class="p">;</span>   <span class="c">/* filter 的 offset parent */</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c">/* main 的 max-width: 70ch 與 margin-inline: auto 由 theme 提供，不動 */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</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"> 8</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">right</span><span class="p">:</span> <span class="nb">calc</span><span class="p">(</span><span class="mi">100</span><span class="kt">%</span> <span class="o">+</span> <span class="mi">2</span><span class="kt">rem</span><span class="p">);</span>   <span class="c">/* main 的左外側、間距 2rem */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">width</span><span class="p">:</span> <span class="mi">400</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>right: calc(100% + 2rem)</code> 的含義：filter 的右邊緣 = main 左邊緣 - 2rem。filter 從這個 anchor 往左展開 400px。</p>
<p>main 永遠按 viewport 置中、filter 永遠貼在 main 左外側。</p>
<hr>
<h2 id="疊層共存的三個關鍵要素">疊層共存的三個關鍵要素</h2>
<h3 id="1-offset-parent-的選擇">1. Offset parent 的選擇</h3>
<p>絕對定位元件的座標相對於「最近的 positioned ancestor」。要讓 sidebar 跟著 main 一起移動（不要跟 viewport 走），就把 main 設為 <code>position: relative</code> 當作 offset parent。</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="nt">body</span><span class="p">.</span><span class="nc">page-search</span> <span class="nt">main</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">2</span><span class="cl"><span class="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span> <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span> <span class="c">/* 相對 main */</span> <span class="p">}</span></span></span></code></pre></div><h3 id="2-anchor-點的選擇">2. Anchor 點的選擇</h3>
<p><code>right: calc(100% + 2rem)</code>、<code>left: -432px</code>、<code>right: 100%; margin-right: 2rem</code> 三種寫法視覺上等價。選擇可讀性最高的 — 通常是 <code>right: calc(100% + 2rem)</code>，意義最直接（「我的右緣 = parent 寬度 + 2rem 之外」）。</p>
<h3 id="3-物理空間檢查">3. 物理空間檢查</h3>
<p>絕對定位不檢查 viewport 邊界 — sidebar 可能被推到 viewport 外。需要在 breakpoint 確認「viewport 夠寬時才顯示 sidebar」：</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="p">.</span><span class="nc">search-filter-slot</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">@</span><span class="k">media</span> <span class="o">(</span><span class="nt">min-width</span><span class="o">:</span> <span class="nt">1400px</span><span class="o">)</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-filter-slot</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">block</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></span></code></pre></div><p>物理空間預算的細節參見〈跨 viewport 雙模式 UI 的物理空間預算〉。</p>
<hr>
<h2 id="內在屬性比較兩種共存做法">內在屬性比較：兩種共存做法</h2>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>排擠式（grid / flex）</th>
          <th>疊層式（absolute）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>中央欄位置</td>
          <td>隨 sidebar 寬度變動</td>
          <td>不受影響、嚴格置中</td>
      </tr>
      <tr>
          <td>Sidebar 寬度限制</td>
          <td>來自 grid container</td>
          <td>來自 viewport（需 breakpoint 控制）</td>
      </tr>
      <tr>
          <td>Layout 重算成本</td>
          <td>改 sidebar 寬度時 main 跟著動</td>
          <td>main 永遠不動</td>
      </tr>
      <tr>
          <td>適用情境</td>
          <td>兩個元件都要自然撐開</td>
          <td>中央嚴格置中、sidebar 附加</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>先確認中央欄的位置要求</strong>。要嚴格置中 → 疊層式；可隨 sidebar 浮動 → 排擠式。</p>
<hr>
<h2 id="設計取捨兩元件共存的-layout-策略">設計取捨：兩元件共存的 layout 策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（疊層 absolute）當預設、其他做法在特定情境合理。</p>
<h3 id="a疊層式中央-layout-flow--附加-absolute這個專案的預設">A：疊層式（中央 layout flow + 附加 absolute）（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：嚴格置中的元件留 layout flow、附加元件 <code>position: absolute</code> 跳出 flow</li>
<li><strong>選 A 的理由</strong>：中央位置不受附加元件影響、改附加元件不需重算中央</li>
<li><strong>適合</strong>：中央嚴格置中 + sidebar 是「附加」的場景（搜尋頁、文章閱讀頁）</li>
<li><strong>代價</strong>：absolute 需要明確 offset parent、要做物理空間檢查避免溢出</li>
</ul>
<h3 id="b排擠式同-layout-flowgrid--flex">B：排擠式（同 layout flow、grid / flex）</h3>
<ul>
<li><strong>機制</strong>：把兩元件放進同一 grid / flex container、各佔一個欄位</li>
<li><strong>跟 A 的取捨</strong>：B 兩元件都自然撐開、A 中央嚴格置中；B 改一邊另一邊跟著動、A 完全解耦</li>
<li><strong>B 比 A 好的情境</strong>：兩元件都是內容主體、需要互相適應寬度（dashboard 多面板）</li>
</ul>
<h3 id="cfixed相對-viewport">C：Fixed（相對 viewport）</h3>
<ul>
<li><strong>機制</strong>：附加元件 <code>position: fixed</code>、永遠在 viewport 同位置</li>
<li><strong>跟 A 的取捨</strong>：C 永遠可見（隨 scroll 不動）、A 跟內容一起 scroll；C 適合 always-on UI</li>
<li><strong>C 比 A 好的情境</strong>：浮動操作鈕、頂部 nav、使用者隨時要 access 的元件</li>
</ul>
<h3 id="d重新設計-layout取消共存需求">D：重新設計 layout（取消共存需求）</h3>
<ul>
<li><strong>機制</strong>：把附加元件搬到完全不同位置（footer、modal、抽屜）</li>
<li><strong>跟 A/B/C 的取捨</strong>：D 完全避開共存問題、A/B/C 解決共存</li>
<li><strong>D 比 A 好的情境</strong>：附加元件使用頻率低（offset 很大價值低）— 例如 settings 改放 modal 內</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>加了 sidebar 後中央內容不再置中</td>
          <td>用了排擠式、中央欄被推</td>
          <td>改用 absolute、main 設 <code>position: relative</code></td>
      </tr>
      <tr>
          <td>Absolute sidebar 跟著 viewport 跑、不貼 main</td>
          <td>沒設 offset parent</td>
          <td>給 main 加 <code>position: relative</code></td>
      </tr>
      <tr>
          <td>Sidebar 在窄 viewport 溢出畫面</td>
          <td>沒做物理空間檢查</td>
          <td>加 breakpoint、寬度不夠時 <code>display: none</code></td>
      </tr>
      <tr>
          <td>改 sidebar 寬度時要回頭調 main 樣式</td>
          <td>排擠式造成 layout 耦合</td>
          <td>改用疊層、main 永遠不需要因 sidebar 改</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：兩個元件的視覺關係用疊層描述、不用排擠描述。疊層 = 兩層獨立 = 改一邊不影響另一邊。</p>
]]></content:encoded></item><item><title>同一個元件在三種互動狀態下顯示位置不同的 root cause</title><link>https://tarrragon.github.io/blog/report/component-tristate-root-cause/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/component-tristate-root-cause/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>元件位置 = 定位基準（anchor）+ 相對基準的偏移。元件「跟著狀態飄」不是元件本身的問題、是它的 anchor 隨狀態在動。&lt;/strong> Debug 時把元件位置拆成「找錨點 → 算偏移」兩層、確認哪一層在隨狀態變化。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼狀態化錯位的根因不在元件本身">為什麼狀態化錯位的根因不在元件本身&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS 計算元件位置時，元件總是「相對某個 reference」 — block flow 是「上一個 sibling 的下緣」、absolute 是 offset parent、grid item 是 grid container。&lt;strong>這個 reference 才是元件位置的決定因素&lt;/strong>。&lt;/p>
&lt;p>當 reference 在不同狀態下尺寸或位置變動，元件被動跟著動 — 看起來是元件「自己飄」，根因卻在 reference。&lt;/p>
&lt;h3 id="三層拆解-debug-法">三層拆解 debug 法&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>問題&lt;/th>
 &lt;th>修法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1. 元件本身&lt;/td>
 &lt;td>元件 CSS 規則錯了？&lt;/td>
 &lt;td>看元件的 computed style&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. 元件的 reference&lt;/td>
 &lt;td>reference 在動嗎？尺寸隨狀態變動？&lt;/td>
 &lt;td>量 reference 在每個狀態下的 bounding rect&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. Reference 的 reference&lt;/td>
 &lt;td>上一層也在動嗎？&lt;/td>
 &lt;td>一層一層往上追&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多數狀態化錯位的根因在第 2 或第 3 層、不在第 1 層。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>新加 scope UI（搜尋範圍 radio group）後出現三個狀態的位置不一致：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>scope UI 位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>初始載入（pagefind 還沒 mount）&lt;/td>
 &lt;td>緊接 H1 下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>點擊 input（focus、空查詢）&lt;/td>
 &lt;td>在 input 與 results 區之間（如預期）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>輸入查詢（results 載入後）&lt;/td>
 &lt;td>跑到所有結果的最下方&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>第一輪猜測：scope UI 自己的 CSS 在不同狀態下不同 — 用 playwright 看 computed style，發現三狀態下 scope 的 grid-row 都是 3、CSS 屬性沒變。&lt;/p>
&lt;p>第二輪：用 playwright &lt;code>getBoundingClientRect()&lt;/code> 量 scope 的位置，發現 y 座標確實在三狀態下不同。&lt;/p>
&lt;p>第三輪：往上一層看「scope 的 grid container 是誰、container 的 grid template 在不同狀態下變了嗎」。發現 search-shell 的 grid template-rows 是 &lt;code>auto&lt;/code>、自動依子元素內容撐開。&lt;/p>
&lt;p>關鍵發現：&lt;strong>&lt;code>.pagefind-ui__drawer&lt;/code> 不是 &lt;code>.pagefind-ui&lt;/code> 的直接子節點 — 它在 &lt;code>&amp;lt;form&amp;gt;&lt;/code> 內&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">.pagefind-ui (display: contents)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">└── form.pagefind-ui__form (grid-row: 2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └── div.pagefind-ui__drawer (grid-row: 4 設了沒生效)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>於是：&lt;/p>
&lt;ul>
&lt;li>初始：form 只含 input、row 2 矮、scope 在 row 3 緊接 row 2 下。&lt;/li>
&lt;li>輸入後：form 含 input + drawer（187 個結果）、row 2 撐到全頁高。grid-row 4 比 row 2 後 — 但 drawer 被 form 包住、整個 form 在 row 2 — scope（row 3）在 form 之後 = 結果之後。&lt;/li>
&lt;/ul>
&lt;p>scope 的 anchor（grid container 的 row 排列）在 form 撐開時改變 — anchor 在動、scope 跟著動。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>元件位置 = 定位基準（anchor）+ 相對基準的偏移。元件「跟著狀態飄」不是元件本身的問題、是它的 anchor 隨狀態在動。</strong> Debug 時把元件位置拆成「找錨點 → 算偏移」兩層、確認哪一層在隨狀態變化。</p>
<hr>
<h2 id="為什麼狀態化錯位的根因不在元件本身">為什麼狀態化錯位的根因不在元件本身</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS 計算元件位置時，元件總是「相對某個 reference」 — block flow 是「上一個 sibling 的下緣」、absolute 是 offset parent、grid item 是 grid container。<strong>這個 reference 才是元件位置的決定因素</strong>。</p>
<p>當 reference 在不同狀態下尺寸或位置變動，元件被動跟著動 — 看起來是元件「自己飄」，根因卻在 reference。</p>
<h3 id="三層拆解-debug-法">三層拆解 debug 法</h3>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>問題</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 元件本身</td>
          <td>元件 CSS 規則錯了？</td>
          <td>看元件的 computed style</td>
      </tr>
      <tr>
          <td>2. 元件的 reference</td>
          <td>reference 在動嗎？尺寸隨狀態變動？</td>
          <td>量 reference 在每個狀態下的 bounding rect</td>
      </tr>
      <tr>
          <td>3. Reference 的 reference</td>
          <td>上一層也在動嗎？</td>
          <td>一層一層往上追</td>
      </tr>
  </tbody>
</table>
<p>多數狀態化錯位的根因在第 2 或第 3 層、不在第 1 層。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>新加 scope UI（搜尋範圍 radio group）後出現三個狀態的位置不一致：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>scope UI 位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>初始載入（pagefind 還沒 mount）</td>
          <td>緊接 H1 下方</td>
      </tr>
      <tr>
          <td>點擊 input（focus、空查詢）</td>
          <td>在 input 與 results 區之間（如預期）</td>
      </tr>
      <tr>
          <td>輸入查詢（results 載入後）</td>
          <td>跑到所有結果的最下方</td>
      </tr>
  </tbody>
</table>
<h3 id="判讀">判讀</h3>
<p>第一輪猜測：scope UI 自己的 CSS 在不同狀態下不同 — 用 playwright 看 computed style，發現三狀態下 scope 的 grid-row 都是 3、CSS 屬性沒變。</p>
<p>第二輪：用 playwright <code>getBoundingClientRect()</code> 量 scope 的位置，發現 y 座標確實在三狀態下不同。</p>
<p>第三輪：往上一層看「scope 的 grid container 是誰、container 的 grid template 在不同狀態下變了嗎」。發現 search-shell 的 grid template-rows 是 <code>auto</code>、自動依子元素內容撐開。</p>
<p>關鍵發現：<strong><code>.pagefind-ui__drawer</code> 不是 <code>.pagefind-ui</code> 的直接子節點 — 它在 <code>&lt;form&gt;</code> 內</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">.pagefind-ui (display: contents)
</span></span><span class="line"><span class="ln">2</span><span class="cl">└── form.pagefind-ui__form (grid-row: 2)
</span></span><span class="line"><span class="ln">3</span><span class="cl">    └── div.pagefind-ui__drawer (grid-row: 4 設了沒生效)</span></span></code></pre></div><p>於是：</p>
<ul>
<li>初始：form 只含 input、row 2 矮、scope 在 row 3 緊接 row 2 下。</li>
<li>輸入後：form 含 input + drawer（187 個結果）、row 2 撐到全頁高。grid-row 4 比 row 2 後 — 但 drawer 被 form 包住、整個 form 在 row 2 — scope（row 3）在 form 之後 = 結果之後。</li>
</ul>
<p>scope 的 anchor（grid container 的 row 排列）在 form 撐開時改變 — anchor 在動、scope 跟著動。</p>
<h3 id="執行">執行</h3>
<p>確認 anchor 問題後改用 absolute 定位：scope 浮在 form 之上、drawer 用 margin-top 讓位。scope 的 anchor 改為 <code>.search-shell</code> 的 <code>position: relative</code>、不再依賴 form 的尺寸。三狀態下位置一致。</p>
<hr>
<h2 id="拆解-anchor-的四個工具">拆解 anchor 的四個工具</h2>
<h3 id="1-找元件的-reference">1. 找元件的 reference</h3>
<table>
  <thead>
      <tr>
          <th>元件的 position</th>
          <th>Reference</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>static（預設）</td>
          <td>上一個 sibling 的下緣 / 父 container</td>
      </tr>
      <tr>
          <td>relative</td>
          <td>元件原本在 flow 中的位置</td>
      </tr>
      <tr>
          <td>absolute</td>
          <td>最近的 positioned ancestor</td>
      </tr>
      <tr>
          <td>fixed</td>
          <td>viewport</td>
      </tr>
      <tr>
          <td>sticky</td>
          <td>滾動容器</td>
      </tr>
      <tr>
          <td>Grid item</td>
          <td>Grid container 的 cell</td>
      </tr>
      <tr>
          <td>Flex item</td>
          <td>Flex container 的軸線</td>
      </tr>
  </tbody>
</table>
<h3 id="2-用-getboundingclientrect-量">2. 用 <code>getBoundingClientRect</code> 量</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="kr">const</span> <span class="nx">el</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">2</span><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">getBoundingClientRect</span><span class="p">());</span></span></span></code></pre></div><p>在三個狀態下分別量、比對 y 座標。差異對應到「reference 在動」。</p>
<h3 id="3-往上追-ancestor-chain">3. 往上追 ancestor chain</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">let</span> <span class="nx">parents</span> <span class="o">=</span> <span class="p">[];</span> <span class="kd">let</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">parents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">tagName</span> <span class="o">+</span> <span class="s1">&#39;.&#39;</span> <span class="o">+</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">el</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">parentElement</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="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">parents</span><span class="p">);</span></span></span></code></pre></div><p>找出 reference 是誰、reference 的 reference 是誰、一層一層追到「不會動」的元素。</p>
<h3 id="4-computed-style-vs-dom-tree-一起看">4. Computed style vs DOM tree 一起看</h3>
<p>CSS 規則在 computed style 顯示為「我設了什麼」、DOM tree 顯示「實際巢狀關係」。兩者一起看才知道規則為什麼沒生效。</p>
<hr>
<h2 id="內在屬性比較三種定位策略對狀態化錯位的抵抗">內在屬性比較：三種定位策略對狀態化錯位的抵抗</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>Anchor 穩定性</th>
          <th>狀態化飄移風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Static / block flow</td>
          <td>低 — 任何前置元素變動都影響</td>
          <td>高 — sibling 撐高就被推下去</td>
      </tr>
      <tr>
          <td>Grid / Flex item</td>
          <td>中 — 跟 container 設計綁定</td>
          <td>中 — container row 撐開時跟著動</td>
      </tr>
      <tr>
          <td>Absolute（自定義 offset parent）</td>
          <td>高 — anchor 是固定 ancestor</td>
          <td>低 — anchor 不變則元件不動</td>
      </tr>
      <tr>
          <td>Fixed</td>
          <td>最高 — anchor 是 viewport</td>
          <td>不會因內容變動飄移、但會因捲動變化</td>
      </tr>
  </tbody>
</table>
<p>當一個元件需要在多種狀態下保持固定位置 — 優先 absolute（搭配明確的 offset parent）。</p>
<hr>
<h2 id="設計取捨對抗狀態化飄移的定位策略">設計取捨：對抗狀態化飄移的定位策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（absolute + 自定義 offset parent）當預設、其他做法在特定情境合理。</p>
<h3 id="aabsolute--穩定-offset-parent這個專案的預設">A：Absolute + 穩定 offset parent（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：元件 <code>position: absolute</code>、選定一個尺寸不隨狀態變動的 ancestor 作為 offset parent</li>
<li><strong>選 A 的理由</strong>：anchor 不變則元件不動、跨所有互動狀態位置一致</li>
<li><strong>適合</strong>：需要在多狀態下保持固定位置的元件</li>
<li><strong>代價</strong>：跳出 layout flow、附近元件需要手動讓位（margin spacer）</li>
</ul>
<h3 id="bgrid--flex-item">B：Grid / Flex item</h3>
<ul>
<li><strong>機制</strong>：把元件當 grid / flex container 的子項、用 grid-row / flex-order 排</li>
<li><strong>跟 A 的取捨</strong>：B 自然 reflow、A 完全 anchor-driven；B 在 container 內容隨狀態撐開時、grid 排序跟著重算</li>
<li><strong>B 比 A 好的情境</strong>：container 尺寸不隨狀態變動的場景（純 layout、內容靜態）</li>
</ul>
<h3 id="cstatic--block-flow預設-layout">C：Static / block flow（預設 layout）</h3>
<ul>
<li><strong>機制</strong>：不設 position、跟 sibling 自然排</li>
<li><strong>跟 A/B 的取捨</strong>：C 最簡單、A/B 主動處理 anchor；C 完全受前置 sibling 影響、狀態化飄移風險最高</li>
<li><strong>C 才合理的情境</strong>：頁面內容極穩定、無狀態切換 — 否則第 N 個元素位置受前 N-1 個元素影響</li>
</ul>
<h3 id="dfixed相對-viewport">D：Fixed（相對 viewport）</h3>
<ul>
<li><strong>機制</strong>：<code>position: fixed</code>、anchor 是 viewport</li>
<li><strong>跟 A 的取捨</strong>：D 永遠在 viewport 同位置、A 跟著內容；D 對「導航類元件」合理、對「內容相關元件」不合理</li>
<li><strong>D 比 A 好的情境</strong>：永遠可見的功能元件（toolbar、scroll-to-top button）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件位置在不同互動狀態下不同</td>
          <td>Anchor 隨狀態變動</td>
          <td>用 playwright 量三個狀態下的 bounding rect</td>
      </tr>
      <tr>
          <td>Computed style 三狀態下都一樣、但位置不同</td>
          <td>Reference 元素的尺寸在動</td>
          <td>量 reference 元素的尺寸、確認哪個狀態下變大</td>
      </tr>
      <tr>
          <td>改元件 CSS 一個狀態好了、另一個壞</td>
          <td>用了 reference-dependent layout</td>
          <td>改用 absolute、選擇穩定的 offset parent</td>
      </tr>
      <tr>
          <td>元件初始正確、互動後跑掉</td>
          <td>Reference 因 reactivity 撐開</td>
          <td>找出該 reference、用 absolute 跳出其影響</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：元件「會飄」不是元件的個性、是它依賴的東西在飄。先找飄的源頭，不要追著元件改。</p>
]]></content:encoded></item><item><title>從色塊 placeholder 開始的漸進式 UI 除錯</title><link>https://tarrragon.github.io/blog/report/placeholder-driven-ui-debug/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/placeholder-driven-ui-debug/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>UI 除錯的最小可驗證單位是「一個有明顯邊界的色塊」。&lt;/strong> 版型用色塊先驗證 grid / flex / absolute 是否如預期排在該在的位置，確定後再串實際內容。一次組裝完整 UI 在版型錯時 debug 困難 — 顏色、字型、邊距、padding 全部一起出問題、根因混雜難辨。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼色塊比實際內容更適合-debug">為什麼色塊比實際內容更適合 debug&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>UI 由「位置、尺寸、視覺樣式、互動」四層組成。Debug 時要分層處理 — 一次只解一層、解完再下一層。&lt;/p>
&lt;p>色塊把後三層都拿掉、只留「位置與尺寸」 — 看到的就是 layout 規則的純粹結果。實際內容把所有層混在一起、看到的位置可能受字型 advance、line-height、margin collapse 等多重因素影響、難以歸因。&lt;/p>
&lt;h3 id="漸進式組裝順序">漸進式組裝順序&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;th>驗證重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>色塊（紅 / 藍背景、固定 width / height）&lt;/td>
 &lt;td>grid / flex / absolute 排對位置嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>加 placeholder 文字&lt;/td>
 &lt;td>文字尺寸符合預期嗎？換行行為對嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>加 padding / border / 圓角等視覺樣式&lt;/td>
 &lt;td>視覺樣式不破壞 layout 嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>換上實際內容 / 接上資料&lt;/td>
 &lt;td>動態內容變動時 layout 還對嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>加互動（hover / click / focus）&lt;/td>
 &lt;td>互動狀態下 layout 還對嗎？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每階段獨立驗證、有問題就停在那階段修。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際應用">這次任務的實際應用&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要驗證搜尋頁的版型：「左側 filter sidebar + 右側中央內容（H1、search input、results）」。&lt;/p>
&lt;p>第一次嘗試：直接把 Pagefind UI 組起來、調 CSS。結果版型錯時不知道是哪層問題 — 是 grid 排序錯？是 sidebar 寬度錯？是 padding 推到位置不對？&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>退回最小可驗證單位：把 filter 整個換成一個寫死寬度的紅色色塊：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">aside&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-filter-debug&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> filter 區（先寫死寬度與底色驗證版型）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">aside&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&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="p">.&lt;/span>&lt;span class="nc">search-filter-debug&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">400&lt;/span>&lt;span class="kt">px&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="k">background&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">red&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="k">min-height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">240&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">position&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">absolute&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="c">/* ... */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>紅色背景一眼看出色塊在哪 — 確認了：&lt;/p>
&lt;ul>
&lt;li>色塊在 main 左外側（符合）&lt;/li>
&lt;li>色塊頂端對齊 H1（符合）&lt;/li>
&lt;li>寬度 400px、與 main 間距 2rem（符合）&lt;/li>
&lt;/ul>
&lt;p>版型驗證後再換上實際 filter UI。&lt;/p>
&lt;h3 id="執行的迭代步驟">執行的迭代步驟&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>步驟&lt;/th>
 &lt;th>動作&lt;/th>
 &lt;th>驗證&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>紅色色塊代替 filter&lt;/td>
 &lt;td>layout 對嗎？看色塊的位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>色塊頂端對齊 results 頂端（用 padding-top）&lt;/td>
 &lt;td>對齊基準對嗎？看頂緣連線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>確認多 viewport 下色塊行為&lt;/td>
 &lt;td>響應式 OK 嗎？拉視窗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>拿掉色塊、JS 把 pagefind filter 搬進來&lt;/td>
 &lt;td>真實內容套上後位置一致嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>細部視覺調整（邊框、間距）&lt;/td>
 &lt;td>視覺樣式 OK 嗎？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每步只驗證一件事、有問題就停。&lt;/p>
&lt;hr>
&lt;h2 id="內在屬性比較兩種除錯起點">內在屬性比較：兩種除錯起點&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>起點&lt;/th>
 &lt;th>Debug 難度&lt;/th>
 &lt;th>修復速度&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一次組裝完整 UI&lt;/td>
 &lt;td>高 — 多層問題交織&lt;/td>
 &lt;td>慢 — 不知該改哪層&lt;/td>
 &lt;td>UI 簡單、一次到位有把握&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>從色塊漸進組裝&lt;/td>
 &lt;td>低 — 每階段問題單純&lt;/td>
 &lt;td>快 — 一次解一個&lt;/td>
 &lt;td>複雜 layout、多元件協作&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>漸進的成本是「多寫一個過渡版本」、收益是「debug 範圍縮到最小」&lt;/strong>。多元件 layout 永遠選漸進。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>UI 除錯的最小可驗證單位是「一個有明顯邊界的色塊」。</strong> 版型用色塊先驗證 grid / flex / absolute 是否如預期排在該在的位置，確定後再串實際內容。一次組裝完整 UI 在版型錯時 debug 困難 — 顏色、字型、邊距、padding 全部一起出問題、根因混雜難辨。</p>
<hr>
<h2 id="為什麼色塊比實際內容更適合-debug">為什麼色塊比實際內容更適合 debug</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>UI 由「位置、尺寸、視覺樣式、互動」四層組成。Debug 時要分層處理 — 一次只解一層、解完再下一層。</p>
<p>色塊把後三層都拿掉、只留「位置與尺寸」 — 看到的就是 layout 規則的純粹結果。實際內容把所有層混在一起、看到的位置可能受字型 advance、line-height、margin collapse 等多重因素影響、難以歸因。</p>
<h3 id="漸進式組裝順序">漸進式組裝順序</h3>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>內容</th>
          <th>驗證重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>色塊（紅 / 藍背景、固定 width / height）</td>
          <td>grid / flex / absolute 排對位置嗎？</td>
      </tr>
      <tr>
          <td>2</td>
          <td>加 placeholder 文字</td>
          <td>文字尺寸符合預期嗎？換行行為對嗎？</td>
      </tr>
      <tr>
          <td>3</td>
          <td>加 padding / border / 圓角等視覺樣式</td>
          <td>視覺樣式不破壞 layout 嗎？</td>
      </tr>
      <tr>
          <td>4</td>
          <td>換上實際內容 / 接上資料</td>
          <td>動態內容變動時 layout 還對嗎？</td>
      </tr>
      <tr>
          <td>5</td>
          <td>加互動（hover / click / focus）</td>
          <td>互動狀態下 layout 還對嗎？</td>
      </tr>
  </tbody>
</table>
<p>每階段獨立驗證、有問題就停在那階段修。</p>
<hr>
<h2 id="這次任務的實際應用">這次任務的實際應用</h2>
<h3 id="觀察">觀察</h3>
<p>要驗證搜尋頁的版型：「左側 filter sidebar + 右側中央內容（H1、search input、results）」。</p>
<p>第一次嘗試：直接把 Pagefind UI 組起來、調 CSS。結果版型錯時不知道是哪層問題 — 是 grid 排序錯？是 sidebar 寬度錯？是 padding 推到位置不對？</p>
<h3 id="判讀">判讀</h3>
<p>退回最小可驗證單位：把 filter 整個換成一個寫死寬度的紅色色塊：</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="p">&lt;</span><span class="nt">aside</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-filter-debug&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  filter 區（先寫死寬度與底色驗證版型）
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">&lt;/</span><span class="nt">aside</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="p">.</span><span class="nc">search-filter-debug</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">width</span><span class="p">:</span> <span class="mi">400</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="kc">red</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">min-height</span><span class="p">:</span> <span class="mi">240</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="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="c">/* ... */</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>紅色背景一眼看出色塊在哪 — 確認了：</p>
<ul>
<li>色塊在 main 左外側（符合）</li>
<li>色塊頂端對齊 H1（符合）</li>
<li>寬度 400px、與 main 間距 2rem（符合）</li>
</ul>
<p>版型驗證後再換上實際 filter UI。</p>
<h3 id="執行的迭代步驟">執行的迭代步驟</h3>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>動作</th>
          <th>驗證</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>紅色色塊代替 filter</td>
          <td>layout 對嗎？看色塊的位置</td>
      </tr>
      <tr>
          <td>2</td>
          <td>色塊頂端對齊 results 頂端（用 padding-top）</td>
          <td>對齊基準對嗎？看頂緣連線</td>
      </tr>
      <tr>
          <td>3</td>
          <td>確認多 viewport 下色塊行為</td>
          <td>響應式 OK 嗎？拉視窗</td>
      </tr>
      <tr>
          <td>4</td>
          <td>拿掉色塊、JS 把 pagefind filter 搬進來</td>
          <td>真實內容套上後位置一致嗎？</td>
      </tr>
      <tr>
          <td>5</td>
          <td>細部視覺調整（邊框、間距）</td>
          <td>視覺樣式 OK 嗎？</td>
      </tr>
  </tbody>
</table>
<p>每步只驗證一件事、有問題就停。</p>
<hr>
<h2 id="內在屬性比較兩種除錯起點">內在屬性比較：兩種除錯起點</h2>
<table>
  <thead>
      <tr>
          <th>起點</th>
          <th>Debug 難度</th>
          <th>修復速度</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一次組裝完整 UI</td>
          <td>高 — 多層問題交織</td>
          <td>慢 — 不知該改哪層</td>
          <td>UI 簡單、一次到位有把握</td>
      </tr>
      <tr>
          <td>從色塊漸進組裝</td>
          <td>低 — 每階段問題單純</td>
          <td>快 — 一次解一個</td>
          <td>複雜 layout、多元件協作</td>
      </tr>
  </tbody>
</table>
<p><strong>漸進的成本是「多寫一個過渡版本」、收益是「debug 範圍縮到最小」</strong>。多元件 layout 永遠選漸進。</p>
<hr>
<h2 id="色塊的設計要點">色塊的設計要點</h2>
<h3 id="1-顏色明顯易於辨識">1. 顏色明顯、易於辨識</h3>
<p>紅色、洋紅、亮藍 — 跟頁面其他元素差異大。debug 完拿掉、不影響正式設計。</p>
<h3 id="2-邊界清楚">2. 邊界清楚</h3>
<p>寫死 width / height / min-height、不要讓色塊「自適應」 — 自適應時看不出色塊本身有沒有按預期擺放（可能是它縮成 0 還是真的擺對位置）。</p>
<h3 id="3-內含可辨識的標籤">3. 內含可辨識的標籤</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">aside</span><span class="p">&gt;</span>filter 區（先寫死寬度與底色驗證版型）<span class="p">&lt;/</span><span class="nt">aside</span><span class="p">&gt;</span></span></span></code></pre></div><p>文字標明這是什麼、目前是「驗證版型」狀態 — 不會被誤認為正式設計。</p>
<h3 id="4-拆解成最小可驗證的單位">4. 拆解成最小可驗證的單位</h3>
<p>要驗證「左欄 + 右欄」就用兩個色塊。不要在第一階段就加 filter 內容、search input 等元件 — 那些是後續階段。</p>
<hr>
<h2 id="設計取捨ui-debug-的起點選擇">設計取捨：UI debug 的起點選擇</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（色塊漸進組裝）當預設、其他做法在特定情境合理。</p>
<h3 id="a色塊-placeholder-漸進組裝這個專案的預設">A：色塊 placeholder 漸進組裝（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：先用寫死寬度的彩色色塊代替每個區塊、確認 layout 後再加內容、再加樣式</li>
<li><strong>選 A 的理由</strong>：每階段只解一個問題、debug 範圍縮到最小</li>
<li><strong>適合</strong>：複雜 layout、多元件協作、不確定 layout 規則對不對</li>
<li><strong>代價</strong>：多一個過渡版本、總時間略長（但 debug 時間短得多）</li>
</ul>
<h3 id="b一次組裝完整-ui">B：一次組裝完整 UI</h3>
<ul>
<li><strong>機制</strong>：直接把 layout + 內容 + 樣式全部寫好、看結果</li>
<li><strong>跟 A 的取捨</strong>：B 一次到位（如果一次對）、A 漸進；B 在版型錯時 debug 困難（多層問題交織）</li>
<li><strong>B 比 A 好的情境</strong>：簡單 layout（&lt; 3 元件、無複雜共存）、有 100% 把握一次到位</li>
</ul>
<h3 id="c用-wireframe-工具figma--sketch">C：用 wireframe 工具（Figma / Sketch）</h3>
<ul>
<li><strong>機制</strong>：先在設計工具畫 wireframe、確認設計後再進實作</li>
<li><strong>跟 A 的取捨</strong>：C 在設計階段確認、A 在實作階段確認；C 適合「設計尚未確定」、A 適合「設計確定但實作有 layout 風險」</li>
<li><strong>C 比 A 好的情境</strong>：設計階段 — 還沒進實作、不確定要做什麼</li>
</ul>
<h3 id="d直接用真實內容-debug-版型">D：直接用真實內容 debug 版型</h3>
<ul>
<li><strong>機制</strong>：拿真實 pagefind UI / 文章內容當 debug 對象</li>
<li><strong>成本特別高的原因</strong>：內容自帶字型 / padding / margin、跟版型問題混在一起、debug 從哪下手都可能錯</li>
<li><strong>D 是反模式</strong>：真實內容適合驗證、不適合 debug — 內容自帶字型 / padding / margin、跟版型問題混在一起、debug 從哪下手都可能錯</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>對應的階段問題</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Layout 一試就錯、不知改哪</td>
          <td>沒做色塊驗證、多層問題交織</td>
          <td>退回色塊 placeholder、單獨驗證 layout</td>
      </tr>
      <tr>
          <td>改 padding 視覺對了、互動後又壞</td>
          <td>樣式調整跑在 layout 確認之前</td>
          <td>退回最簡 layout、確認穩定後再加樣式</td>
      </tr>
      <tr>
          <td>真實內容套上後位置變了</td>
          <td>內容尺寸跟色塊預設不一樣</td>
          <td>量真實內容尺寸、回頭調 layout 規則或固定容器尺寸</td>
      </tr>
      <tr>
          <td>Debug 時間遠超估算</td>
          <td>起點選錯（從複雜 UI 開始）</td>
          <td>退到色塊重來、會比繼續調快</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：UI 除錯的速度跟「起點的簡單度」成正比。從色塊出發、永遠比從完整 UI 出發快。</p>
<p>跟 <a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a> 的關係：placeholder 漸進是 Checkpoint 2「開發中」的具體做法 — 每階段只引入一個變數、邏輯錯誤跟視覺錯誤能即時 catch。跳階段（直接寫真實內容 + 完整樣式）= 把開發中 checkpoint collapse 成單次驗收、漏掉的失敗會推到 ship 前 / ship 後。</p>
]]></content:encoded></item><item><title>在開發循環裡早一點用 playwright 看真實結果</title><link>https://tarrragon.github.io/blog/report/playwright-early-in-loop/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/playwright-early-in-loop/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Playwright 不是最後手段、是縮短診斷迴圈的工具。&lt;/strong> 當靜態 CSS 推理 + 視覺截圖溝通的循環失敗 ≥ 2 次、就應該停止推理、改用 playwright &lt;code>browser_evaluate&lt;/code> 直接讀 live DOM 與 computed style。早一點用 = 試錯次數更少、心智負擔更輕。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼推理迴圈有極限">為什麼推理迴圈有極限&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS 行為由「規則 + DOM tree + 樣式繼承 + 框架渲染」四個變數共同決定。靜態推理只能基於假設的 DOM tree — 假設錯了、推理就錯。視覺截圖溝通只能傳達「結果是什麼」、無法傳達「為什麼是這個結果」。&lt;/p>
&lt;p>Playwright 的 &lt;code>browser_evaluate&lt;/code> 直接執行 JS 在 live page、返回真實的 DOM tree、computed style、bounding rect — &lt;strong>把「四個變數」全部變成已知&lt;/strong>。&lt;/p>
&lt;h3 id="推理-vs-量測的成本曲線">推理 vs 量測的成本曲線&lt;/h3>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>靜態推理 + 截圖&lt;/td>
 &lt;td>快 — 假設正確時一次到位&lt;/td>
 &lt;td>慢 — 假設錯了得重來&lt;/td>
 &lt;td>越來越慢 — 假設錯誤累積&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Playwright 量測&lt;/td>
 &lt;td>中 — 起 server、寫 evaluate&lt;/td>
 &lt;td>快 — server 已在跑&lt;/td>
 &lt;td>快 — 重用 setup&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第 1 次推理快、後續成本爆炸；playwright 起步慢、後續穩定。&lt;strong>門檻在第 2 次&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把 search scope UI 放在「搜尋輸入框與結果之間」。&lt;/p>
&lt;p>第一輪：基於 class name 推測 DOM tree、用 grid + display:contents 設 grid-row 排序。第二輪：發現 scope 跑到頁尾、嘗試調 grid-template-rows。第三輪：嘗試 absolute 定位但時機不對。第四輪：使用者說「思路錯了」、要我換方向。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>四輪推理都基於同一個假設：&lt;code>drawer&lt;/code> 是 &lt;code>.pagefind-ui&lt;/code> 的直接子節點、跟 &lt;code>form&lt;/code> 並列。實際用 playwright 一查：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">let&lt;/span> &lt;span class="nx">parents&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span> &lt;span class="kd">let&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">drawer&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="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&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="nx">parents&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tagName&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parentElement&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&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">DIV.pagefind-ui__drawer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">FORM.pagefind-ui__form ← drawer 在 form 內！
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">DIV.pagefind-ui&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>假設錯了 — drawer 是 form 的 child、不是 sibling。grid 規則無論怎麼寫都不會生效，因為 drawer 跟 form 共用同一個 grid cell。&lt;/p>
&lt;p>四輪推理 ≈ 30 分鐘。Playwright 一次查清楚 ≈ 2 分鐘。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>
&lt;p>確認 DOM 結構後：grid 不適合這個場景、改用 absolute + drawer margin-top spacer。一次到位。&lt;/p>
&lt;hr>
&lt;h2 id="playwright-在開發循環的三個位置">Playwright 在開發循環的三個位置&lt;/h2>
&lt;h3 id="1-假設驗證">1. 假設驗證&lt;/h3>
&lt;p>寫 CSS 規則前先量 DOM、確認結構符合假設。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">parents&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[].&lt;/span>&lt;span class="nx">slice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&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="kd">let&lt;/span> &lt;span class="nx">chain&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span> &lt;span class="kd">let&lt;/span> &lt;span class="nx">n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&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="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">chain&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tagName&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">n&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">n&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parentElement&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">5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">chain&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">})&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-行為驗證">2. 行為驗證&lt;/h3>
&lt;p>Layout 規則寫完後驗證實際結果。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">rect&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">getBoundingClientRect&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">computed&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">getComputedStyle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">gridRow&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-互動驗證">3. 互動驗證&lt;/h3>
&lt;p>驗證使用者互動後的狀態。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;pre&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dispatchEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">Event&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">bubbles&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">}));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nb">Promise&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">setTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1000&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nb">Array&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">from&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.result&amp;#39;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">getComputedStyle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">display&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="s1">&amp;#39;none&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">map&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">));&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="內在屬性比較四種-debug-方法">內在屬性比較：四種 debug 方法&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>靜態 CSS 推理&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>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>瀏覽器 DevTools&lt;/td>
 &lt;td>高 — DOM + computed&lt;/td>
 &lt;td>中 — 每次手點&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Playwright &lt;code>browser_evaluate&lt;/code>&lt;/td>
 &lt;td>最高 — 程式化任意查詢&lt;/td>
 &lt;td>低 — 改 query 重跑&lt;/td>
 &lt;td>是 — 同樣 query 可寫測試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇順序：&lt;strong>簡單 layout 用 DevTools；複雜 / 反覆 debug 用 playwright；推理只在第 1 次試錯前&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Playwright 不是最後手段、是縮短診斷迴圈的工具。</strong> 當靜態 CSS 推理 + 視覺截圖溝通的循環失敗 ≥ 2 次、就應該停止推理、改用 playwright <code>browser_evaluate</code> 直接讀 live DOM 與 computed style。早一點用 = 試錯次數更少、心智負擔更輕。</p>
<hr>
<h2 id="為什麼推理迴圈有極限">為什麼推理迴圈有極限</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS 行為由「規則 + DOM tree + 樣式繼承 + 框架渲染」四個變數共同決定。靜態推理只能基於假設的 DOM tree — 假設錯了、推理就錯。視覺截圖溝通只能傳達「結果是什麼」、無法傳達「為什麼是這個結果」。</p>
<p>Playwright 的 <code>browser_evaluate</code> 直接執行 JS 在 live page、返回真實的 DOM tree、computed style、bounding rect — <strong>把「四個變數」全部變成已知</strong>。</p>
<h3 id="推理-vs-量測的成本曲線">推理 vs 量測的成本曲線</h3>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>第 1 次嘗試</th>
          <th>第 2 次</th>
          <th>第 3 次以上</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>靜態推理 + 截圖</td>
          <td>快 — 假設正確時一次到位</td>
          <td>慢 — 假設錯了得重來</td>
          <td>越來越慢 — 假設錯誤累積</td>
      </tr>
      <tr>
          <td>Playwright 量測</td>
          <td>中 — 起 server、寫 evaluate</td>
          <td>快 — server 已在跑</td>
          <td>快 — 重用 setup</td>
      </tr>
  </tbody>
</table>
<p>第 1 次推理快、後續成本爆炸；playwright 起步慢、後續穩定。<strong>門檻在第 2 次</strong>。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>要把 search scope UI 放在「搜尋輸入框與結果之間」。</p>
<p>第一輪：基於 class name 推測 DOM tree、用 grid + display:contents 設 grid-row 排序。第二輪：發現 scope 跑到頁尾、嘗試調 grid-template-rows。第三輪：嘗試 absolute 定位但時機不對。第四輪：使用者說「思路錯了」、要我換方向。</p>
<h3 id="判讀">判讀</h3>
<p>四輪推理都基於同一個假設：<code>drawer</code> 是 <code>.pagefind-ui</code> 的直接子節點、跟 <code>form</code> 並列。實際用 playwright 一查：</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="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">2</span><span class="cl"><span class="kd">let</span> <span class="nx">parents</span> <span class="o">=</span> <span class="p">[];</span> <span class="kd">let</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">drawer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nx">el</span> <span class="o">&amp;&amp;</span> <span class="nx">el</span> <span class="o">!==</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">parents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">tagName</span> <span class="o">+</span> <span class="s1">&#39;.&#39;</span> <span class="o">+</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">el</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">parentElement</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>返回：</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">DIV.pagefind-ui__drawer
</span></span><span class="line"><span class="ln">2</span><span class="cl">FORM.pagefind-ui__form    ← drawer 在 form 內！
</span></span><span class="line"><span class="ln">3</span><span class="cl">DIV.pagefind-ui</span></span></code></pre></div><p>假設錯了 — drawer 是 form 的 child、不是 sibling。grid 規則無論怎麼寫都不會生效，因為 drawer 跟 form 共用同一個 grid cell。</p>
<p>四輪推理 ≈ 30 分鐘。Playwright 一次查清楚 ≈ 2 分鐘。</p>
<h3 id="執行">執行</h3>
<p>確認 DOM 結構後：grid 不適合這個場景、改用 absolute + drawer margin-top spacer。一次到位。</p>
<hr>
<h2 id="playwright-在開發循環的三個位置">Playwright 在開發循環的三個位置</h2>
<h3 id="1-假設驗證">1. 假設驗證</h3>
<p>寫 CSS 規則前先量 DOM、確認結構符合假設。</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="kr">async</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="nx">parents</span><span class="o">:</span> <span class="p">[].</span><span class="nx">slice</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">)).</span><span class="nx">map</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kd">let</span> <span class="nx">chain</span> <span class="o">=</span> <span class="p">[];</span> <span class="kd">let</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">el</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="nx">n</span><span class="p">)</span> <span class="p">{</span> <span class="nx">chain</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">n</span><span class="p">.</span><span class="nx">tagName</span> <span class="o">+</span> <span class="s1">&#39;.&#39;</span> <span class="o">+</span> <span class="nx">n</span><span class="p">.</span><span class="nx">className</span><span class="p">);</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">chain</span><span class="p">;</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 class="p">})</span></span></span></code></pre></div><h3 id="2-行為驗證">2. 行為驗證</h3>
<p>Layout 規則寫完後驗證實際結果。</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="kr">async</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="nx">rect</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;.target&#39;</span><span class="p">).</span><span class="nx">getBoundingClientRect</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">computed</span><span class="o">:</span> <span class="nx">getComputedStyle</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">)).</span><span class="nx">gridRow</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">})</span></span></span></code></pre></div><h3 id="3-互動驗證">3. 互動驗證</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="kr">async</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">input</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-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">input</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s1">&#39;pre&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">input</span><span class="p">.</span><span class="nx">dispatchEvent</span><span class="p">(</span><span class="k">new</span> <span class="nx">Event</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">bubbles</span><span class="o">:</span> <span class="kc">true</span> <span class="p">}));</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="kr">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">r</span> <span class="p">=&gt;</span> <span class="nx">setTimeout</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span> <span class="mi">1000</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">return</span> <span class="nb">Array</span><span class="p">.</span><span class="nx">from</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.result&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="nx">getComputedStyle</span><span class="p">(</span><span class="nx">el</span><span class="p">).</span><span class="nx">display</span> <span class="o">!==</span> <span class="s1">&#39;none&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="nx">el</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">50</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><hr>
<h2 id="內在屬性比較四種-debug-方法">內在屬性比較：四種 debug 方法</h2>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>取得資訊量</th>
          <th>重複成本</th>
          <th>可寫成測試</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>靜態 CSS 推理</td>
          <td>低 — 全是假設</td>
          <td>高 — 每次重思考</td>
          <td>否</td>
      </tr>
      <tr>
          <td>視覺截圖溝通</td>
          <td>中 — 只有結果</td>
          <td>中 — 截圖 / 描述慢</td>
          <td>否</td>
      </tr>
      <tr>
          <td>瀏覽器 DevTools</td>
          <td>高 — DOM + computed</td>
          <td>中 — 每次手點</td>
          <td>否</td>
      </tr>
      <tr>
          <td>Playwright <code>browser_evaluate</code></td>
          <td>最高 — 程式化任意查詢</td>
          <td>低 — 改 query 重跑</td>
          <td>是 — 同樣 query 可寫測試</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>簡單 layout 用 DevTools；複雜 / 反覆 debug 用 playwright；推理只在第 1 次試錯前</strong>。</p>
<hr>
<h2 id="引入-playwright-的最低門檻">引入 playwright 的最低門檻</h2>





<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"># 啟動本地 server（任何方式）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 -m http.server <span class="m">8000</span> --directory public
</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="c1"># 或專案有 hugo</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">hugo server</span></span></code></pre></div><p>Playwright MCP 提供：</p>
<ul>
<li><code>browser_navigate(url)</code> — 開頁</li>
<li><code>browser_evaluate(fn)</code> — 執行 JS 拿結果</li>
<li><code>browser_take_screenshot()</code> — 截圖</li>
<li><code>browser_snapshot()</code> — accessibility tree</li>
</ul>
<p>寫一個 evaluate fn ≈ 30 行 JS，比反覆推理快得多。</p>
<hr>
<h2 id="設計取捨css--dom-debug-工具選擇">設計取捨：CSS / DOM debug 工具選擇</h2>
<p>四種做法、各自機會成本不同。這個專案在推理 ≥ 2 次失敗後選 A（playwright <code>browser_evaluate</code>）當預設、其他做法在特定情境合理。</p>
<blockquote>
<p>本篇是 <a href="../two-occurrence-threshold/">#42 2 次門檻</a> 抽象原則在「debug 工具切換」這個面向的應用。</p></blockquote>
<h3 id="aplaywright-browser_evaluate-程式化讀-live-dom這個專案的預設">A：Playwright <code>browser_evaluate</code> 程式化讀 live DOM（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：起 server、用 <code>browser_evaluate</code> 寫 JS query 讀 DOM tree / computed style / bounding rect</li>
<li><strong>選 A 的理由</strong>：取得資訊量最大、可重跑、可寫成測試</li>
<li><strong>適合</strong>：推理失敗 ≥ 2 次、複雜或反覆 debug 的情境</li>
<li><strong>代價</strong>：起步成本中（需要 server + 寫 evaluate）</li>
</ul>
<h3 id="b靜態-css-推理--視覺截圖溝通">B：靜態 CSS 推理 + 視覺截圖溝通</h3>
<ul>
<li><strong>機制</strong>：純看 CSS 與假設的 DOM 推測、用截圖跟使用者溝通</li>
<li><strong>跟 A 的取捨</strong>：B 起步成本 0、A 起步成本中；但 B 第 2 次以後成本爆炸（每輪都基於前輪錯誤假設）</li>
<li><strong>B 比 A 好的情境</strong>：第 1 次嘗試、預估假設正確機率高（簡單修改）</li>
</ul>
<h3 id="c瀏覽器-devtools-手動查">C：瀏覽器 DevTools 手動查</h3>
<ul>
<li><strong>機制</strong>：開 DevTools 切 Elements / Computed / Layout 面板手動探索</li>
<li><strong>跟 A 的取捨</strong>：C 不需 server / playwright setup、但每次手點切面板慢、不能寫成測試</li>
<li><strong>C 比 A 好的情境</strong>：一次性確認、不需要重複 query 同樣資訊</li>
</ul>
<h3 id="d寫成-playwright-測試固化">D：寫成 playwright 測試固化</h3>
<ul>
<li><strong>機制</strong>：把 debug 過程寫成 playwright 測試、未來自動跑</li>
<li><strong>跟 A 的取捨</strong>：D 是 A 的延伸 — 第 2 次 debug 同個版型時、值得固化（<a href="../layout-tests-with-playwright/">#15 layout tests</a>）</li>
<li><strong>D 比 A 好的情境</strong>：版型 bug 出現第 2 次以上、值得寫測試防止回歸</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>工具切換時機</th>
          <th>第一個該寫的 evaluate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>推理 ≥ 2 次失敗</td>
          <td>切到 playwright</td>
          <td>量目標元素的 ancestor chain</td>
      </tr>
      <tr>
          <td>Layout 在某些狀態下錯、其他狀態下對</td>
          <td>切到 playwright</td>
          <td>量該元素在不同狀態下的 bounding rect</td>
      </tr>
      <tr>
          <td>改 CSS 不生效、specificity 看起來對</td>
          <td>切到 playwright</td>
          <td>量 computed style 看真正套到的值</td>
      </tr>
      <tr>
          <td>動態 DOM 結構不確定</td>
          <td>切到 playwright</td>
          <td>列出目標 container 的子節點</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：縮短診斷迴圈的工具該早一點用、不該等到推理徹底失敗。第 2 次推理失敗就切換、別等第 5 次。</p>
<p>延伸應用：playwright 也用來查「資料層 vs 視覺層的層錯位」 — 見 <a href="../view-layer-filter-vs-source-layer/">#55 Filter 與 Source 的抽象層錯位</a> 用 <code>browser_evaluate</code> 量 source 真實 cardinality 與分批機制。</p>
]]></content:encoded></item><item><title>排版精度的工具選擇：CSS-only vs JS-assisted</title><link>https://tarrragon.github.io/blog/report/css-only-vs-js-assisted/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/css-only-vs-js-assisted/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>CSS 處理 build-time 可決定的 layout、JS 處理 runtime 才知道的尺寸與 stateful DOM 移動。&lt;/strong> 邊界誤判：硬要 CSS 解決 runtime 問題會反覆試錯；硬要 JS 解決 layout 問題會跟 framework 渲染競爭。&lt;/p>
&lt;p>選擇問題簡化為兩問：&lt;/p>
&lt;ol>
&lt;li>這個值在 build time 能定下來嗎？能 → CSS；不能 → JS 量測寫回 CSS 變數。&lt;/li>
&lt;li>這個 DOM 變動是 framework 管的嗎？是 → 不要動；不是 → JS 可動。&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="為什麼分工是必要的">為什麼分工是必要的&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS 的設計假設是「規則在 build time 決定、瀏覽器渲染時應用」。CSS 沒有 reactive 機制 — 沒辦法「等元素渲染完才知道高度然後對齊」。&lt;/p>
&lt;p>JS 的設計假設是「runtime 可以讀寫 DOM 與 style」。JS 可以在元件渲染後量測尺寸、可以隨 viewport 變動 reparent 節點。&lt;/p>
&lt;p>&lt;strong>用錯工具不只「不太優雅」、是直接做不到&lt;/strong>。要 CSS 解決動態尺寸只能寫 magic number（猜的）；要 JS 解決靜態 layout 寫了一堆 imperative 代碼還可能跟 framework 衝突。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的工具分配">這次任務的工具分配&lt;/h2>
&lt;h3 id="css-處理的部分">CSS 處理的部分&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>任務&lt;/th>
 &lt;th>CSS 寫法&lt;/th>
 &lt;th>為什麼用 CSS&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>H1 / search input 的固定高度&lt;/td>
 &lt;td>&lt;code>height: 64px&lt;/code> 寫死&lt;/td>
 &lt;td>Build time 可決定的設計 token&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>搜尋頁主欄置中、breakpoint 切換&lt;/td>
 &lt;td>&lt;code>@media (min-width: 1400px)&lt;/code>&lt;/td>
 &lt;td>純宣告式 layout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter sidebar absolute 定位&lt;/td>
 &lt;td>&lt;code>position: absolute; right: calc(100% + 2rem)&lt;/code>&lt;/td>
 &lt;td>靜態定位關係&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Drawer 留出 scope 空間&lt;/td>
 &lt;td>&lt;code>margin-top: calc(var(--search-scope-h) + 8px)&lt;/code>&lt;/td>
 &lt;td>引用變數的 calc&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="js-處理的部分">JS 處理的部分&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>任務&lt;/th>
 &lt;th>JS 寫法&lt;/th>
 &lt;th>為什麼用 JS&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>量測 scope 高度寫回 CSS 變數&lt;/td>
 &lt;td>ResizeObserver&lt;/td>
 &lt;td>Runtime 才知道（字型、換行）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter sidebar 切換到 mobile drawer&lt;/td>
 &lt;td>matchMedia + appendChild&lt;/td>
 &lt;td>跨 viewport 的 stateful DOM 移動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope filter（regex 比對標題 / 內文）&lt;/td>
 &lt;td>event listener + setProperty&lt;/td>
 &lt;td>純 runtime 邏輯、無 build time 解&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope UI 寫死值與量測值的橋&lt;/td>
 &lt;td>&lt;code>style.setProperty('--search-scope-h', ...)&lt;/code>&lt;/td>
 &lt;td>JS 寫回讓 CSS 用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="兩問判斷法">兩問判斷法&lt;/h2>
&lt;h3 id="問-1這個值在-build-time-能定下來嗎">問 1：這個值在 build time 能定下來嗎&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>值&lt;/th>
 &lt;th>Build time 知道嗎&lt;/th>
 &lt;th>工具&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>設計 token（spacing、typography scale）&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>CSS 變數寫死&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件固定尺寸（icon size、button height）&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>CSS height / width&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>響應式 breakpoint&lt;/td>
 &lt;td>是（設計決定）&lt;/td>
 &lt;td>&lt;code>@media&lt;/code> query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>動態文字塊高度（受字型 / 換行）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>JS ResizeObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件位置（隨 viewport 變化）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>JS getBoundingClientRect&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>知道 → CSS 解。不知道 → JS 量測寫回 CSS 變數、CSS 從變數計算。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>CSS 處理 build-time 可決定的 layout、JS 處理 runtime 才知道的尺寸與 stateful DOM 移動。</strong> 邊界誤判：硬要 CSS 解決 runtime 問題會反覆試錯；硬要 JS 解決 layout 問題會跟 framework 渲染競爭。</p>
<p>選擇問題簡化為兩問：</p>
<ol>
<li>這個值在 build time 能定下來嗎？能 → CSS；不能 → JS 量測寫回 CSS 變數。</li>
<li>這個 DOM 變動是 framework 管的嗎？是 → 不要動；不是 → JS 可動。</li>
</ol>
<hr>
<h2 id="為什麼分工是必要的">為什麼分工是必要的</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS 的設計假設是「規則在 build time 決定、瀏覽器渲染時應用」。CSS 沒有 reactive 機制 — 沒辦法「等元素渲染完才知道高度然後對齊」。</p>
<p>JS 的設計假設是「runtime 可以讀寫 DOM 與 style」。JS 可以在元件渲染後量測尺寸、可以隨 viewport 變動 reparent 節點。</p>
<p><strong>用錯工具不只「不太優雅」、是直接做不到</strong>。要 CSS 解決動態尺寸只能寫 magic number（猜的）；要 JS 解決靜態 layout 寫了一堆 imperative 代碼還可能跟 framework 衝突。</p>
<hr>
<h2 id="這次任務的工具分配">這次任務的工具分配</h2>
<h3 id="css-處理的部分">CSS 處理的部分</h3>
<table>
  <thead>
      <tr>
          <th>任務</th>
          <th>CSS 寫法</th>
          <th>為什麼用 CSS</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>H1 / search input 的固定高度</td>
          <td><code>height: 64px</code> 寫死</td>
          <td>Build time 可決定的設計 token</td>
      </tr>
      <tr>
          <td>搜尋頁主欄置中、breakpoint 切換</td>
          <td><code>@media (min-width: 1400px)</code></td>
          <td>純宣告式 layout</td>
      </tr>
      <tr>
          <td>Filter sidebar absolute 定位</td>
          <td><code>position: absolute; right: calc(100% + 2rem)</code></td>
          <td>靜態定位關係</td>
      </tr>
      <tr>
          <td>Drawer 留出 scope 空間</td>
          <td><code>margin-top: calc(var(--search-scope-h) + 8px)</code></td>
          <td>引用變數的 calc</td>
      </tr>
  </tbody>
</table>
<h3 id="js-處理的部分">JS 處理的部分</h3>
<table>
  <thead>
      <tr>
          <th>任務</th>
          <th>JS 寫法</th>
          <th>為什麼用 JS</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>量測 scope 高度寫回 CSS 變數</td>
          <td>ResizeObserver</td>
          <td>Runtime 才知道（字型、換行）</td>
      </tr>
      <tr>
          <td>Filter sidebar 切換到 mobile drawer</td>
          <td>matchMedia + appendChild</td>
          <td>跨 viewport 的 stateful DOM 移動</td>
      </tr>
      <tr>
          <td>Scope filter（regex 比對標題 / 內文）</td>
          <td>event listener + setProperty</td>
          <td>純 runtime 邏輯、無 build time 解</td>
      </tr>
      <tr>
          <td>Scope UI 寫死值與量測值的橋</td>
          <td><code>style.setProperty('--search-scope-h', ...)</code></td>
          <td>JS 寫回讓 CSS 用</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="兩問判斷法">兩問判斷法</h2>
<h3 id="問-1這個值在-build-time-能定下來嗎">問 1：這個值在 build time 能定下來嗎</h3>
<table>
  <thead>
      <tr>
          <th>值</th>
          <th>Build time 知道嗎</th>
          <th>工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計 token（spacing、typography scale）</td>
          <td>是</td>
          <td>CSS 變數寫死</td>
      </tr>
      <tr>
          <td>元件固定尺寸（icon size、button height）</td>
          <td>是</td>
          <td>CSS height / width</td>
      </tr>
      <tr>
          <td>響應式 breakpoint</td>
          <td>是（設計決定）</td>
          <td><code>@media</code> query</td>
      </tr>
      <tr>
          <td>動態文字塊高度（受字型 / 換行）</td>
          <td>否</td>
          <td>JS ResizeObserver</td>
      </tr>
      <tr>
          <td>元件位置（隨 viewport 變化）</td>
          <td>否</td>
          <td>JS getBoundingClientRect</td>
      </tr>
  </tbody>
</table>
<p>知道 → CSS 解。不知道 → JS 量測寫回 CSS 變數、CSS 從變數計算。</p>
<h3 id="問-2這個-dom-變動是-framework-管的嗎">問 2：這個 DOM 變動是 framework 管的嗎</h3>
<table>
  <thead>
      <tr>
          <th>變動</th>
          <th>Framework 管</th>
          <th>工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自家 DOM 內元素加 / 移 / 改</td>
          <td>否</td>
          <td>JS 自由動</td>
      </tr>
      <tr>
          <td>Framework 元素的整節點 reparent</td>
          <td>不管內部</td>
          <td>JS 可搬</td>
      </tr>
      <tr>
          <td>Framework 元素內部的子節點</td>
          <td>是</td>
          <td>不要動</td>
      </tr>
      <tr>
          <td>Framework 元素的 attribute</td>
          <td>視 framework 而定</td>
          <td>通常不要動</td>
      </tr>
  </tbody>
</table>
<p>是 → 不要動，用 CSS 視覺解。不是 → JS 可動。</p>
<hr>
<h2 id="內在屬性比較兩種工具的特性">內在屬性比較：兩種工具的特性</h2>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>CSS-only</th>
          <th>JS-assisted</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>知識成本</td>
          <td>低（語言簡單）</td>
          <td>中（需要 DOM API）</td>
      </tr>
      <tr>
          <td>執行時機</td>
          <td>渲染前 / 樣式重新計算</td>
          <td>DOMContentLoaded 後 / 事件觸發</td>
      </tr>
      <tr>
          <td>是否阻塞首次渲染</td>
          <td>是（CSS 是 render-blocking）</td>
          <td>否（async）</td>
      </tr>
      <tr>
          <td>Framework 衝突風險</td>
          <td>無</td>
          <td>有（若動到 framework 管的 DOM）</td>
      </tr>
      <tr>
          <td>可維護性</td>
          <td>高（純 declarative）</td>
          <td>中（imperative）</td>
      </tr>
      <tr>
          <td>跨瀏覽器一致性</td>
          <td>高（CSS 標準清楚）</td>
          <td>中（API 差異）</td>
      </tr>
  </tbody>
</table>
<p>優先 CSS — declarative、無 framework 衝突、首次渲染就生效。JS 補 CSS 做不到的部分。</p>
<hr>
<h2 id="邊界誤判的兩種失敗">邊界誤判的兩種失敗</h2>
<h3 id="css-解-runtime-問題">CSS 解 runtime 問題</h3>
<p>例：用 CSS magic number 寫死 scope-h（猜 56px），實際渲染 73.5px、對齊壞掉。</p>
<p>修法：認知到「scope-h 是 runtime 才能知道的值」、改用 ResizeObserver 量測寫回 CSS 變數。</p>
<h3 id="js-解-framework-managed-layout">JS 解 framework-managed layout</h3>
<p>例：用 JS <code>appendChild</code> 把 scope UI 注入 <code>.pagefind-ui</code> 內、Svelte 重繪時清掉。</p>
<p>修法：認知到「<code>.pagefind-ui</code> 是 framework 邊界內」、改用 CSS absolute 把 scope 浮在外部。</p>
<hr>
<h2 id="設計取捨css--js-工具分配策略">設計取捨：CSS / JS 工具分配策略</h2>
<p>四種做法、各自機會成本不同。這個專案以 A（CSS-first、JS 透過變數補）為主、其他做法在特定情境合理。</p>
<h3 id="acss-firstjs-透過-css-變數補-runtime-值這個專案的預設">A：CSS-first、JS 透過 CSS 變數補 runtime 值（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：CSS 處理 build time 可決定的 layout、JS 量測 runtime 值寫回 CSS 變數、CSS 從變數計算</li>
<li><strong>選 A 的理由</strong>：CSS 是 declarative 不阻塞首次渲染、跟 framework 不衝突；變數是 JS-CSS 介面、單向資料流</li>
<li><strong>適合</strong>：絕大多數客製情境</li>
<li><strong>代價</strong>：需要明確分辨「值能在 build time 定下來嗎」、JS 寫法要是「寫回變數」而不是「直接改 inline style」</li>
</ul>
<h3 id="b純-css-only無-js">B：純 CSS-only（無 JS）</h3>
<ul>
<li><strong>機制</strong>：純宣告式、不寫 JS</li>
<li><strong>跟 A 的取捨</strong>：B 完全無 JS 維護成本、A 多一層 JS；但 B 對 runtime 才知道的尺寸（內容動態）束手無策</li>
<li><strong>B 比 A 好的情境</strong>：所有值都能 build time 確定、layout 完全靜態</li>
</ul>
<h3 id="cjs-imperative-layout每次重算">C：JS-imperative layout（每次重算）</h3>
<ul>
<li><strong>機制</strong>：JS 監聽變動、每次重新計算所有元素位置、寫 inline style</li>
<li><strong>成本特別高的原因</strong>：imperative 代碼難維護、首次渲染慢（要等 JS 跑）、跟 framework 渲染衝突</li>
<li><strong>C 才合理的情境</strong>：CSS 表達不出的複雜 layout（極罕見、現代 CSS 已涵蓋大多數）</li>
</ul>
<h3 id="dcss-magic-number-估算-runtime-值">D：CSS magic number 估算 runtime 值</h3>
<ul>
<li><strong>機制</strong>：CSS 內寫死「應該差不多」的值（不量測）</li>
<li><strong>跟 A 的取捨</strong>：D 看似省 JS、實際對齊在邊界情境（字型 / theme）必壞</li>
<li><strong>D 是反模式</strong>：「runtime 才知道的值」用 CSS 估算是錯誤工具選擇 — 對齊在邊界情境（字型 / theme）必壞</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>工具誤用方向</th>
          <th>修正動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CSS 寫了 magic number、改字型後對不齊</td>
          <td>用 CSS 解 runtime 問題</td>
          <td>量測該值、改 ResizeObserver 寫回變數</td>
      </tr>
      <tr>
          <td>JS 寫了 100+ 行做 layout</td>
          <td>用 JS 解靜態 layout 問題</td>
          <td>退回 CSS、用 grid / flex / absolute 達成</td>
      </tr>
      <tr>
          <td>JS 改 framework DOM 後，framework 更新就失效</td>
          <td>JS 動到 framework 管的領域</td>
          <td>改用 CSS 視覺定位、不動 framework DOM</td>
      </tr>
      <tr>
          <td>Inline style 散落多處難 debug</td>
          <td>JS 直接寫 style 而非透過變數</td>
          <td>重構成「JS 寫 CSS 變數、CSS 從變數計算」</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：選工具不是品味問題、是「值能不能在 build time 定下來」「DOM 是不是我管」兩個技術問題的答案。問清楚再選。</p>
]]></content:encoded></item><item><title>JS 操作 framework 元件：邊界辨識與安全規則</title><link>https://tarrragon.github.io/blog/report/component-boundary-and-js-impact/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/component-boundary-and-js-impact/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>JS 操作 framework 元件前先界定邊界、選對應的安全規則執行。&lt;/strong> 邊界 = 契約 = 安全範圍。整節點搬遷安全、改節點內部不安全、改節點 attribute 是灰區。每類操作有對應的安全規則 — 不是「能不能動」、是「動了之後 framework 會不會 revert」。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>framework 元件本身需要動時的安全規則&lt;/strong>。「客製 UI 該放哪」由 &lt;a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留 framework 邊界外&lt;/a> 處理 — 預設應該完全不動 framework、需要動時才參考本篇。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼邊界要先界定">為什麼邊界要先界定&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>每個元件（自家或 framework 提供）有「對外契約」與「內部實作」。對外契約包括：&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>DOM identity&lt;/td>
 &lt;td>哪些 class / id / attribute 是穩定的&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State 來源&lt;/td>
 &lt;td>元件內部 state 由誰寫、何時改&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>渲染週期&lt;/td>
 &lt;td>元件何時重繪、重繪時影響哪些 DOM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對外介面&lt;/td>
 &lt;td>提供哪些 props / events / API hooks&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>JS 操作前不知道這些 = 黑箱操作。動了什麼、會觸發什麼、誰會被影響、不可預測。&lt;/p>
&lt;h3 id="邊界宣告的格式">邊界宣告的格式&lt;/h3>
&lt;p>開始 JS 操作之前、寫一段註解或 mental note：&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">動什麼：filter-panel 的 parent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">邊界：filter-panel 整個節點 OK，內部子節點屬於 pagefind 管
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">State：checkbox 勾選狀態存在 panel 子節點上、由 pagefind 維護
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">動作：appendChild 整節點 reparent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">為什麼安全：節點 identity 不變、pagefind 在下次 patch 時看到節點還在&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段宣告把「動什麼」「不能動什麼」「為什麼安全」說清楚 — 不是儀式、是強迫自己想清楚再動。&lt;/p>
&lt;hr>
&lt;h2 id="三類操作的安全度">三類操作的安全度&lt;/h2>
&lt;p>從最安全到最不安全：&lt;/p>
&lt;h3 id="1-整節點-reparent安全">1. 整節點 reparent（安全）&lt;/h3>
&lt;p>把 framework 管的整個節點搬到別處 — 節點 identity 不變、framework 在下次 patch 時仍認得它。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 安全 — 整節點搬位置
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">sidebar&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-改節點內部子節點不安全">2. 改節點內部子節點（不安全）&lt;/h3>
&lt;p>在 framework 管的節點內 appendChild / removeChild / 改子節點屬性 — framework 會 revert。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 不安全 — 在 framework 子樹內加東西
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">myCustomDiv&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.x&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-y&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;z&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-改節點自身的-attribute--inline-style灰區">3. 改節點自身的 attribute / inline style（灰區）&lt;/h3>
&lt;p>改 framework 管的節點本身的 attribute、看 framework 是否認為這屬於 reactive：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 灰區 — 看 framework 怎麼處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">display&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;none&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;my-state&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;aria-hidden&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下節展開三類各自的設計細節。&lt;/p>
&lt;hr>
&lt;h2 id="整節點-reparent為什麼安全怎麼做才安全">整節點 reparent：為什麼安全、怎麼做才安全&lt;/h2>
&lt;h3 id="為什麼安全">為什麼安全&lt;/h3>
&lt;p>Framework 的 reconciliation 通常以「節點 identity」為依據 — &lt;strong>同一個節點在哪裡不重要、節點存不存在才重要&lt;/strong>。&lt;/p>
&lt;p>把 &lt;code>.pagefind-ui__filter-panel&lt;/code> 從 drawer 移到外部 aside：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Framework 看到&lt;/th>
 &lt;th>反應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>節點還在（identity 沒變）&lt;/td>
 &lt;td>繼續更新它的內部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點的 parent 變了&lt;/td>
 &lt;td>Framework 不關心 — parent 不在 component tree 內&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點內的 children 不變&lt;/td>
 &lt;td>Framework 不需要重建&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>reconciliation 不會因為「位置變了」而重建節點 — 重建只發生在「節點消失了 + 新節點出現」的情境。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>JS 操作 framework 元件前先界定邊界、選對應的安全規則執行。</strong> 邊界 = 契約 = 安全範圍。整節點搬遷安全、改節點內部不安全、改節點 attribute 是灰區。每類操作有對應的安全規則 — 不是「能不能動」、是「動了之後 framework 會不會 revert」。</p>
<blockquote>
<p>本篇焦點：<strong>framework 元件本身需要動時的安全規則</strong>。「客製 UI 該放哪」由 <a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留 framework 邊界外</a> 處理 — 預設應該完全不動 framework、需要動時才參考本篇。</p></blockquote>
<hr>
<h2 id="為什麼邊界要先界定">為什麼邊界要先界定</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>每個元件（自家或 framework 提供）有「對外契約」與「內部實作」。對外契約包括：</p>
<table>
  <thead>
      <tr>
          <th>契約類型</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM identity</td>
          <td>哪些 class / id / attribute 是穩定的</td>
      </tr>
      <tr>
          <td>State 來源</td>
          <td>元件內部 state 由誰寫、何時改</td>
      </tr>
      <tr>
          <td>渲染週期</td>
          <td>元件何時重繪、重繪時影響哪些 DOM</td>
      </tr>
      <tr>
          <td>對外介面</td>
          <td>提供哪些 props / events / API hooks</td>
      </tr>
  </tbody>
</table>
<p>JS 操作前不知道這些 = 黑箱操作。動了什麼、會觸發什麼、誰會被影響、不可預測。</p>
<h3 id="邊界宣告的格式">邊界宣告的格式</h3>
<p>開始 JS 操作之前、寫一段註解或 mental note：</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-panel 的 parent
</span></span><span class="line"><span class="ln">2</span><span class="cl">邊界：filter-panel 整個節點 OK，內部子節點屬於 pagefind 管
</span></span><span class="line"><span class="ln">3</span><span class="cl">State：checkbox 勾選狀態存在 panel 子節點上、由 pagefind 維護
</span></span><span class="line"><span class="ln">4</span><span class="cl">動作：appendChild 整節點 reparent
</span></span><span class="line"><span class="ln">5</span><span class="cl">為什麼安全：節點 identity 不變、pagefind 在下次 patch 時看到節點還在</span></span></code></pre></div><p>這段宣告把「動什麼」「不能動什麼」「為什麼安全」說清楚 — 不是儀式、是強迫自己想清楚再動。</p>
<hr>
<h2 id="三類操作的安全度">三類操作的安全度</h2>
<p>從最安全到最不安全：</p>
<h3 id="1-整節點-reparent安全">1. 整節點 reparent（安全）</h3>
<p>把 framework 管的整個節點搬到別處 — 節點 identity 不變、framework 在下次 patch 時仍認得它。</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="nx">sidebar</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span></span></span></code></pre></div><h3 id="2-改節點內部子節點不安全">2. 改節點內部子節點（不安全）</h3>
<p>在 framework 管的節點內 appendChild / removeChild / 改子節點屬性 — framework 會 revert。</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">// 不安全 — 在 framework 子樹內加東西
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">filterPanel</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">myCustomDiv</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">filterPanel</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.x&#39;</span><span class="p">).</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-y&#39;</span><span class="p">,</span> <span class="s1">&#39;z&#39;</span><span class="p">);</span></span></span></code></pre></div><h3 id="3-改節點自身的-attribute--inline-style灰區">3. 改節點自身的 attribute / inline style（灰區）</h3>
<p>改 framework 管的節點本身的 attribute、看 framework 是否認為這屬於 reactive：</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">// 灰區 — 看 framework 怎麼處理
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s1">&#39;none&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;my-state&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;aria-hidden&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span></span></span></code></pre></div><p>下節展開三類各自的設計細節。</p>
<hr>
<h2 id="整節點-reparent為什麼安全怎麼做才安全">整節點 reparent：為什麼安全、怎麼做才安全</h2>
<h3 id="為什麼安全">為什麼安全</h3>
<p>Framework 的 reconciliation 通常以「節點 identity」為依據 — <strong>同一個節點在哪裡不重要、節點存不存在才重要</strong>。</p>
<p>把 <code>.pagefind-ui__filter-panel</code> 從 drawer 移到外部 aside：</p>
<table>
  <thead>
      <tr>
          <th>Framework 看到</th>
          <th>反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>節點還在（identity 沒變）</td>
          <td>繼續更新它的內部</td>
      </tr>
      <tr>
          <td>節點的 parent 變了</td>
          <td>Framework 不關心 — parent 不在 component tree 內</td>
      </tr>
      <tr>
          <td>節點內的 children 不變</td>
          <td>Framework 不需要重建</td>
      </tr>
  </tbody>
</table>
<p>reconciliation 不會因為「位置變了」而重建節點 — 重建只發生在「節點消失了 + 新節點出現」的情境。</p>
<h3 id="安全-reparent-的-do--dont">安全 reparent 的 do / don&rsquo;t</h3>
<table>
  <thead>
      <tr>
          <th>Do</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>parent.appendChild(node)</code> 整節點搬</td>
          <td>identity 保留</td>
      </tr>
      <tr>
          <td><code>parent.insertBefore(node, ref)</code> 整節點搬到特定位置</td>
          <td>identity 保留</td>
      </tr>
      <tr>
          <td>搬之前 <code>node.cloneNode(true)</code> 為複本（如果要保留原位）</td>
          <td>複本是新 identity、原節點仍由 framework 管</td>
      </tr>
      <tr>
          <td>搬完後不動 node 內部</td>
          <td>framework 繼續正常更新</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th>Don&rsquo;t</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>parent.appendChild(node.firstChild)</code> 搬 framework 子節點</td>
          <td>把節點抽出原 parent、framework 認為消失了</td>
      </tr>
      <tr>
          <td><code>node.innerHTML = node.innerHTML</code> 重設內部</td>
          <td>創造一堆新 identity、framework 認不得</td>
      </tr>
      <tr>
          <td>搬完後在 node 內 appendChild 加東西</td>
          <td>加的東西不在 framework 認知中、被清</td>
      </tr>
      <tr>
          <td>搬完後改 node 內子節點的 text / attribute</td>
          <td>framework 在下次 patch 時 revert</td>
      </tr>
  </tbody>
</table>
<p><strong>核心規則</strong>：搬節點 = 操作 node 本身；不要操作 node 的 children。</p>
<h3 id="跟-framework-reactivity-的對齊">跟 framework reactivity 的對齊</h3>
<p>某些 framework 對節點的「內部值」是 reactive 的（例如 <code>&lt;input&gt;</code> 的 <code>value</code>），改了會被 reconcile 回來。對這類屬性：</p>
<table>
  <thead>
      <tr>
          <th>屬性類型</th>
          <th>操作策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reactive value（input.value、textContent）</td>
          <td>透過 framework API 改、不要直接改 DOM</td>
      </tr>
      <tr>
          <td>純展示 attribute（class、aria-* 多數情境）</td>
          <td>直接改 DOM 通常 OK、但仍是灰區（見下節）</td>
      </tr>
      <tr>
          <td>Layout-relevant style（display、position）</td>
          <td>直接改 DOM 通常 OK、可能需要 fail-safe</td>
      </tr>
  </tbody>
</table>
<p>不確定某屬性是否 reactive：讀框架 source / 文件確認、或加 fail-safe 防意外。</p>
<hr>
<h2 id="改節點內部為什麼不安全有什麼例外">改節點內部：為什麼不安全、有什麼例外</h2>
<h3 id="為什麼不安全">為什麼不安全</h3>
<p>在 framework 管的節點內 appendChild / removeChild — framework 不認得這些操作的結果、下次 patch 時：</p>
<ol>
<li>Framework 比對 component tree 的目標狀態</li>
<li>看到 DOM 多了不該有的節點 → 移除</li>
<li>或看到 DOM 少了該有的節點 → 重建</li>
</ol>
<p>我們手動加的節點屬於前者、被移除。我們手動移除的子節點屬於後者、被重建（且重建的 identity 不同）。</p>
<h3 id="唯一的例外靜態元件--確認-patch-不重設">唯一的例外：靜態元件 + 確認 patch 不重設</h3>
<p>如果該 framework 子樹「初次 mount 後不再 patch」、改內部可能安全。但這是<strong>框架實作細節、隨版本可能變動</strong>。</p>
<p>例：當前 pagefind 的 filter 順序在初次 mount 時生成、後續 patch 不重排 — 所以 reorder filter 子節點實際安全。但這是「當前版本碰巧」、不是「框架保證」。</p>
<p><strong>操作規則</strong>：不要依賴「碰巧安全」。如果必須改內部、加 MutationObserver 監聽 framework 是否 revert、必要時補打。</p>
<hr>
<h2 id="改節點-attribute--inline-style灰區的-fail-safe-設計">改節點 attribute / inline style：灰區的 fail-safe 設計</h2>
<h3 id="為什麼是灰區">為什麼是灰區</h3>
<p>Framework 通常<strong>不主動管理節點的 inline style 與非 reactive attribute</strong> — 但「通常」不是「永遠」。某些 framework 會在 patch 時把 inline style 重設、或把 attribute 跟 component state 強制同步。</p>
<h3 id="fail-safe-工具-1important-提升優先級">Fail-safe 工具 1：<code>!important</code> 提升優先級</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="nx">el</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;display&#39;</span><span class="p">,</span> <span class="s1">&#39;none&#39;</span><span class="p">,</span> <span class="s1">&#39;important&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>important</code> 把 inline style 的優先級提升 — 即使 framework 套了同屬性的低優先 style、也蓋不過。</p>
<h3 id="fail-safe-工具-2mutationobserver-補打">Fail-safe 工具 2：MutationObserver 補打</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">reapply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">el</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;display&#39;</span><span class="p">,</span> <span class="s1">&#39;none&#39;</span><span class="p">,</span> <span class="s1">&#39;important&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">reapply</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">reapply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">parent</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>Framework 在重繪後可能把 element 替換成新的 — observer 監聽到變動、立刻補套 style。</p>
<p>詳細設計（observer 範圍 / 觸發頻率 / self-mutation 處理）由 <a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率</a> 處理。</p>
<h3 id="fail-safe-工具-3css-class-toggle-取代-inline-style">Fail-safe 工具 3：CSS class toggle 取代 inline style</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="c1">// 不用 inline style
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s1">&#39;is-hidden&#39;</span><span class="p">);</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">/* CSS 內定義行為、layered CSS 不需要 important */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">@</span><span class="k">layer</span> <span class="nt">base</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">is-hidden</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</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></span></code></pre></div><p>詳細展開由 <a href="../class-toggle-over-important/">#28 class toggle 取代 important</a> 處理。</p>
<p><strong>選擇順序</strong>：能用 class toggle 就用（最乾淨）；framework 會清 class 才用 inline + important + observer。</p>
<hr>
<h2 id="這次任務的邊界辨識實例">這次任務的邊界辨識實例</h2>
<p>四個 JS 操作場景、各有不同邊界：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>動的對象</th>
          <th>操作類別</th>
          <th>安全度</th>
          <th>處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把 filter-panel 從 drawer 搬到 sidebar</td>
          <td>整節點 reparent</td>
          <td>1（安全）</td>
          <td>高</td>
          <td>直接搬、不動內部</td>
      </tr>
      <tr>
          <td>Reorder type / tag filter</td>
          <td>filter 子節點順序</td>
          <td>2（不安全）</td>
          <td>中 — 視 framework 而定</td>
          <td>確認框架不 reset 順序、加 observer 防護</td>
      </tr>
      <tr>
          <td>注入 scope UI</td>
          <td>自家新元件</td>
          <td>N/A（自家領域）</td>
          <td>高</td>
          <td>放 framework 邊界外（<a href="../coexisting-with-framework-managed-dom/">#5</a>）</td>
      </tr>
      <tr>
          <td>Filter 結果 hide / show</td>
          <td>pagefind 結果元素的 display</td>
          <td>3（灰區）</td>
          <td>中</td>
          <td>inline + important + observer 補打</td>
      </tr>
  </tbody>
</table>
<p>每個場景操作前的 mental check：「這是哪一類？該用什麼安全規則？」</p>
<hr>
<h2 id="設計取捨操作-framework-元件的策略">設計取捨：操作 framework 元件的策略</h2>
<p>四種策略、各自機會成本不同。預設追求「最高安全度的方式達成需求」、成本太高再降級。</p>
<h3 id="a完全不動-framework客製留邊界外這個專案的預設">A：完全不動 framework、客製留邊界外（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：把客製 UI 放在 framework sibling 位置、用 CSS 達成視覺效果</li>
<li><strong>選 A 的理由</strong>：跟 framework 完全解耦、命運自主</li>
<li><strong>適合</strong>：需求是「在 framework 旁加東西」（多數情境）</li>
<li><strong>代價</strong>：CSS 定位可能複雜</li>
<li><strong>詳細</strong>：<a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留 framework 邊界外</a></li>
</ul>
<h3 id="b整節點-reparent">B：整節點 reparent</h3>
<ul>
<li><strong>機制</strong>：把 framework 管的節點搬位置、不動內部</li>
<li><strong>跟 A 的取捨</strong>：A 不動 framework、B 搬 framework 元件本身；B 換到的是「能改變 framework 元件位置」、付出的是「節點內部仍由 framework 管、外部行為仍可能變」</li>
<li><strong>B 比 A 好的情境</strong>：framework 元件位置決定權需要奪回（例如 sidebar 切換）</li>
</ul>
<h3 id="c改節點-attribute--fail-safe">C：改節點 attribute + fail-safe</h3>
<ul>
<li><strong>機制</strong>：改 inline style / class、加 important + observer 補打</li>
<li><strong>跟 A/B 的取捨</strong>：A 不碰 framework、C 介入 framework 元件本身的視覺行為；C 比 A 侵入性高、但比直接改內部安全</li>
<li><strong>C 比 B 好的情境</strong>：需要的不是搬位置、是改顯隱 / 顏色 / state</li>
</ul>
<h3 id="d改節點內部最後手段">D：改節點內部（最後手段）</h3>
<ul>
<li><strong>機制</strong>：在 framework 子樹內 appendChild、改子節點屬性</li>
<li><strong>成本特別高的原因</strong>：跟 framework reconciliation 直接競爭、bug 不可預測、升級可能徹底打破</li>
<li><strong>D 才合理的情境</strong>：當前 framework 確認「該子樹不 reconcile」+ 升級時會重新驗證 — 通常不值得</li>
</ul>
<hr>
<h2 id="邊界宣告的實踐">邊界宣告的實踐</h2>
<h3 id="寫成-jsdoc-或-inline-註解">寫成 JSDoc 或 inline 註解</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="cm">/**
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cm"> * 把 .pagefind-ui__filter-panel 從 drawer 搬到外部 sidebar。
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="cm"> * 邊界：
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="cm"> *   - 動：filter-panel 整節點的 parent
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="cm"> *   - 不動：filter-panel 內部子節點（由 pagefind 管）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="cm"> *   - State：checkbox 勾選由 pagefind 維護、跟著節點走
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="cm"> * 為什麼安全：節點 identity 不變、pagefind 在下次 patch
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="cm"> * 時看到節點還在、繼續更新內部。
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">sidebar</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>註解是給未來的自己 / 同事看的「契約備忘」 — 看到操作時知道為什麼安全。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>抽象層原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a></td>
          <td>本篇是「邊界內 DOM 層」操作的具體規則 — 接受要進入這層、用本篇規則限制傷害</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>操作範圍越小越安全 — 整節點 reparent 比改內部範圍小、改 attribute 比改子樹範圍小</td>
      </tr>
      <tr>
          <td><a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留邊界外</a></td>
          <td>互補關係 — #5 處理「不動 framework 的策略」、本篇處理「必須動 framework 時的安全規則」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>邊界問題</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JS 操作後 framework 行為異常</td>
          <td>動到內部子節點</td>
          <td>確認操作只動「整節點 identity」、不動內部</td>
      </tr>
      <tr>
          <td>Inline style 在某些互動後消失</td>
          <td>動到 framework 管的 attribute</td>
          <td>加 observer 補打、或改用 CSS class toggle</td>
      </tr>
      <tr>
          <td>reparent 後 framework state 重置</td>
          <td>整節點移動但 framework 看作刪除</td>
          <td>確認框架對節點 identity 的追蹤機制（少數框架不靠 identity）</td>
      </tr>
      <tr>
          <td>某些 querySelector 命中不該命中的元素</td>
          <td>Selector 範圍超過自家元件</td>
          <td>把 query 限縮到 self 元件根節點下（<a href="../dom-selector-precision/">#14 selector 精準度</a>）</td>
      </tr>
      <tr>
          <td>「再加一段防禦邏輯應該就好了」第 2 次</td>
          <td>整體策略可能該換層級（從 D 升到 C 或 B）</td>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a>、考慮換策略</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：JS 動 framework 元件前、邊界先界定、選對應的安全規則。預設追求「完全不動 framework」(A)、必須動時用層級遞減的策略（B / C / D）— 每往下一層付的是「跟 framework 競爭」的成本。</p>
]]></content:encoded></item><item><title>Selector 精準度：讓 query 只命中你想要的元素</title><link>https://tarrragon.github.io/blog/report/dom-selector-precision/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/dom-selector-precision/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>JS 的 DOM query 從具體開始、發現不夠用再放寬。&lt;/strong> Selector 涵蓋「最少必要範圍」、避免誤命中其他元素、避免未來頁面結構變動讓 query 撈到不該撈的東西。精準度有三個收斂維度：起點（從哪開始找）、範圍（找多深）、過濾（哪些不要）— 三者一起設計才完整。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼精準度是-default">為什麼精準度是 default&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>DOM selector 的範圍越寬、被誤命中的可能性越高。寬泛 selector 像「網撈」 — 當下頁面只有一個目標元素時看不出問題、未來頁面結構變動（加第二個同類元件、加 demo 區塊、加 widget）就壞。&lt;/p>
&lt;p>精準度的成本是「寫 selector 時多想一點」、收益是「行為可預測、不會被未來變動打破」。&lt;strong>這不是優化、是 sanity 防線&lt;/strong>。&lt;/p>
&lt;h3 id="寬泛-selector-的失敗模式">寬泛 selector 的失敗模式&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗模式&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>根因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>跨元件誤命中&lt;/td>
 &lt;td>該動的動了、不該動的也動了&lt;/td>
 &lt;td>沒指定 ancestor scope&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同名 class 誤命中&lt;/td>
 &lt;td>demo 區塊 / 文檔截圖也被處理&lt;/td>
 &lt;td>沒過濾「處於展示用途」的元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>未初始化元素被處理&lt;/td>
 &lt;td>元件還沒 mount 完就被操作&lt;/td>
 &lt;td>沒過濾「狀態未就緒」的元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已處理元素重複處理&lt;/td>
 &lt;td>apply 被 observer 觸發又處理一次&lt;/td>
 &lt;td>沒標記「已處理」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四種失敗都來自「query 範圍 &amp;gt; 真實需要的範圍」。從具體開始就避免。&lt;/p>
&lt;hr>
&lt;h2 id="三層收斂維度">三層收斂維度&lt;/h2>
&lt;p>Selector 精準度不是單一參數、是三個維度的組合。每個維度都該設計、不能只想其中一個。&lt;/p>
&lt;h3 id="維度-1起點從哪個-root-開始找">維度 1：起點（從哪個 root 開始找）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：query 的起點決定「最大可能範圍」。從 &lt;code>document&lt;/code> 起 = 全頁面；從元件根起 = 子樹內。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 寬：全頁面搜尋
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 收斂：從元件根開始
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從元件根開始等於把 selector 的作用範圍收斂到「我管的子樹」 — 即使未來頁面其他地方出現同名元素、跟我無關。&lt;/p>
&lt;p>&lt;strong>起點選擇的決策&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>起點&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>document&lt;/code>&lt;/td>
 &lt;td>確定全頁只有一個目標、且未來不會增加同類&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件根（變數存好）&lt;/td>
 &lt;td>一般情境（推薦預設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>函式參數傳入根&lt;/td>
 &lt;td>同頁面有多個元件實例、各自獨立 setup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件 &lt;code>closest&lt;/code> 反向找根&lt;/td>
 &lt;td>動態多實例、用事件驅動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>多元件 setup pattern&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 其他 setup
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>頁面有 N 個 shell、自動 setup N 次、各自獨立。當前只一個也適用、未來加更多無痛 — 這是「起點當參數」帶來的擴展性。&lt;/p>
&lt;p>&lt;strong>例外處理&lt;/strong>：當目標元素不在元件子樹內（例如同層的 sibling），保留 &lt;code>document.querySelector&lt;/code> 但加註解說明：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// slot 是 main 的子節點、跟 shell 同層、不能從 shell 找
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">slot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>註解讓未來維護者知道這是「明知故為」的例外、不是疏忽。&lt;/p>
&lt;h3 id="維度-2範圍找多深">維度 2：範圍（找多深）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：起點確定後、要找直接子、特定層、還是任意深度。&lt;/p>
&lt;p>&lt;code>querySelector&lt;/code> 預設找任意深度 — 大部分情況沒問題、但結構穩定時可以更精準：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 預設：任意深度
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 限縮：只找直接子
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;:scope &amp;gt; .pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">// 限縮：只找特定層
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;:scope &amp;gt; div &amp;gt; .pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>:scope&lt;/code> 在 querySelector 內表示 query 的起始元素 — 配合 &lt;code>&amp;gt;&lt;/code> 就能精準匹配「直接子」。&lt;/p>
&lt;p>&lt;strong>範圍選擇的決策&lt;/strong>：&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>結構可能變動、目標可能搬位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>直接子 &lt;code>:scope &amp;gt; X&lt;/code>&lt;/td>
 &lt;td>結構穩定、避免深層誤命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>特定路徑 &lt;code>:scope &amp;gt; A &amp;gt; B&lt;/code>&lt;/td>
 &lt;td>結構非常穩定、想要結構變動立即察覺&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選太寬未來誤命中、選太窄未來結構微調就壞 — 預設選任意深度、結構穩定的關鍵 query 才用 &lt;code>:scope &amp;gt;&lt;/code>。&lt;/p>
&lt;h3 id="維度-3過濾哪些元素不要">維度 3：過濾（哪些元素不要）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：起點 + 範圍確定後、可能還是命中過多 — 用 attribute filter 與否定 selector 排除不要的。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>JS 的 DOM query 從具體開始、發現不夠用再放寬。</strong> Selector 涵蓋「最少必要範圍」、避免誤命中其他元素、避免未來頁面結構變動讓 query 撈到不該撈的東西。精準度有三個收斂維度：起點（從哪開始找）、範圍（找多深）、過濾（哪些不要）— 三者一起設計才完整。</p>
<hr>
<h2 id="為什麼精準度是-default">為什麼精準度是 default</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>DOM selector 的範圍越寬、被誤命中的可能性越高。寬泛 selector 像「網撈」 — 當下頁面只有一個目標元素時看不出問題、未來頁面結構變動（加第二個同類元件、加 demo 區塊、加 widget）就壞。</p>
<p>精準度的成本是「寫 selector 時多想一點」、收益是「行為可預測、不會被未來變動打破」。<strong>這不是優化、是 sanity 防線</strong>。</p>
<h3 id="寬泛-selector-的失敗模式">寬泛 selector 的失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>表現</th>
          <th>根因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨元件誤命中</td>
          <td>該動的動了、不該動的也動了</td>
          <td>沒指定 ancestor scope</td>
      </tr>
      <tr>
          <td>同名 class 誤命中</td>
          <td>demo 區塊 / 文檔截圖也被處理</td>
          <td>沒過濾「處於展示用途」的元素</td>
      </tr>
      <tr>
          <td>未初始化元素被處理</td>
          <td>元件還沒 mount 完就被操作</td>
          <td>沒過濾「狀態未就緒」的元素</td>
      </tr>
      <tr>
          <td>已處理元素重複處理</td>
          <td>apply 被 observer 觸發又處理一次</td>
          <td>沒標記「已處理」</td>
      </tr>
  </tbody>
</table>
<p>四種失敗都來自「query 範圍 &gt; 真實需要的範圍」。從具體開始就避免。</p>
<hr>
<h2 id="三層收斂維度">三層收斂維度</h2>
<p>Selector 精準度不是單一參數、是三個維度的組合。每個維度都該設計、不能只想其中一個。</p>
<h3 id="維度-1起點從哪個-root-開始找">維度 1：起點（從哪個 root 開始找）</h3>
<p><strong>核心定義</strong>：query 的起點決定「最大可能範圍」。從 <code>document</code> 起 = 全頁面；從元件根起 = 子樹內。</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="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</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="c1">// 收斂：從元件根開始
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">shell</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-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">);</span></span></span></code></pre></div><p>從元件根開始等於把 selector 的作用範圍收斂到「我管的子樹」 — 即使未來頁面其他地方出現同名元素、跟我無關。</p>
<p><strong>起點選擇的決策</strong>：</p>
<table>
  <thead>
      <tr>
          <th>起點</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>document</code></td>
          <td>確定全頁只有一個目標、且未來不會增加同類</td>
      </tr>
      <tr>
          <td>元件根（變數存好）</td>
          <td>一般情境（推薦預設）</td>
      </tr>
      <tr>
          <td>函式參數傳入根</td>
          <td>同頁面有多個元件實例、各自獨立 setup</td>
      </tr>
      <tr>
          <td>事件 <code>closest</code> 反向找根</td>
          <td>動態多實例、用事件驅動</td>
      </tr>
  </tbody>
</table>
<p><strong>多元件 setup pattern</strong>：</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="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</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">5</span><span class="cl">  <span class="c1">// ... 其他 setup
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span></span></span></code></pre></div><p>頁面有 N 個 shell、自動 setup N 次、各自獨立。當前只一個也適用、未來加更多無痛 — 這是「起點當參數」帶來的擴展性。</p>
<p><strong>例外處理</strong>：當目標元素不在元件子樹內（例如同層的 sibling），保留 <code>document.querySelector</code> 但加註解說明：</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">// slot 是 main 的子節點、跟 shell 同層、不能從 shell 找
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">slot</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-filter-slot&#39;</span><span class="p">);</span></span></span></code></pre></div><p>註解讓未來維護者知道這是「明知故為」的例外、不是疏忽。</p>
<h3 id="維度-2範圍找多深">維度 2：範圍（找多深）</h3>
<p><strong>核心定義</strong>：起點確定後、要找直接子、特定層、還是任意深度。</p>
<p><code>querySelector</code> 預設找任意深度 — 大部分情況沒問題、但結構穩定時可以更精準：</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="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</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="c1">// 限縮：只找直接子
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;:scope &gt; .pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">// 限縮：只找特定層
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;:scope &gt; div &gt; .pagefind-ui&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>:scope</code> 在 querySelector 內表示 query 的起始元素 — 配合 <code>&gt;</code> 就能精準匹配「直接子」。</p>
<p><strong>範圍選擇的決策</strong>：</p>
<table>
  <thead>
      <tr>
          <th>範圍</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>任意深度（預設）</td>
          <td>結構可能變動、目標可能搬位置</td>
      </tr>
      <tr>
          <td>直接子 <code>:scope &gt; X</code></td>
          <td>結構穩定、避免深層誤命中</td>
      </tr>
      <tr>
          <td>特定路徑 <code>:scope &gt; A &gt; B</code></td>
          <td>結構非常穩定、想要結構變動立即察覺</td>
      </tr>
  </tbody>
</table>
<p>選太寬未來誤命中、選太窄未來結構微調就壞 — 預設選任意深度、結構穩定的關鍵 query 才用 <code>:scope &gt;</code>。</p>
<h3 id="維度-3過濾哪些元素不要">維度 3：過濾（哪些元素不要）</h3>
<p><strong>核心定義</strong>：起點 + 範圍確定後、可能還是命中過多 — 用 attribute filter 與否定 selector 排除不要的。</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">// 寬：所有 result
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</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="c1">// 過濾：只取已 rank 過的（排除初始化中的）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result[data-pagefind-rank]&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">// 過濾：排除已處理過的
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result:not([data-scoped])&#39;</span><span class="p">);</span></span></span></code></pre></div><p><strong>過濾技巧</strong>：</p>
<table>
  <thead>
      <tr>
          <th>技巧</th>
          <th>用法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Attribute filter</td>
          <td><code>[data-state=&quot;ready&quot;]</code> 只取狀態就緒的</td>
      </tr>
      <tr>
          <td><code>:not()</code> 排除</td>
          <td><code>:not([data-scoped])</code> 排除已處理</td>
      </tr>
      <tr>
          <td>Attribute exists</td>
          <td><code>[data-pagefind-rank]</code> 只取有特定屬性的</td>
      </tr>
      <tr>
          <td>處理後標記</td>
          <td>處理完 <code>el.setAttribute('data-scoped', 'true')</code> 避免重複處理</td>
      </tr>
  </tbody>
</table>
<p><strong>「處理後標記」是 idempotency 工具</strong>：apply 函式可能被多次呼叫（observer 觸發、event 觸發），標記 + <code>:not()</code> 過濾確保每個元素只處理一次。</p>
<hr>
<h2 id="三維度的組合範例">三維度的組合範例</h2>
<p>完整的精準 selector 設計：</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="kd">var</span> <span class="nx">shell</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-shell&#39;</span><span class="p">);</span>           <span class="c1">// 維度 1：起點
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</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="kd">var</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span>                          <span class="c1">// 維度 2：任意深度
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;.pagefind-ui__result[data-pagefind-rank]:not([data-scoped])&#39;</span>  <span class="c1">// 維度 3：過濾
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">results</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>                      <span class="c1">// 處理後標記
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>每個維度都有意識地選擇 — 不是把所有預設值疊一起。</p>
<hr>
<h2 id="內在屬性比較四種-selector-設計">內在屬性比較：四種 selector 設計</h2>
<table>
  <thead>
      <tr>
          <th>設計</th>
          <th>誤命中風險</th>
          <th>未來結構變動的容忍度</th>
          <th>多元件支援</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>document.querySelector('.x')</code></td>
          <td>高</td>
          <td>低 — 任何同名出現就壞</td>
          <td>否（只取第一個）</td>
      </tr>
      <tr>
          <td><code>shell.querySelector('.x')</code></td>
          <td>低</td>
          <td>中 — shell 內變動才影響</td>
          <td>部分</td>
      </tr>
      <tr>
          <td><code>shell.querySelector(':scope &gt; .x')</code></td>
          <td>最低</td>
          <td>低 — 結構微調就壞</td>
          <td>部分</td>
      </tr>
      <tr>
          <td>起點當參數 + 過濾 + 標記</td>
          <td>最低</td>
          <td>高 — 顯式聲明所有假設</td>
          <td>完整</td>
      </tr>
  </tbody>
</table>
<p><strong>推薦</strong>：起點當參數 + 過濾。<code>:scope &gt;</code> 只在「結構保證穩定」的關鍵 query 用。</p>
<hr>
<h2 id="進階技巧">進階技巧</h2>
<h3 id="1-把元件根存成變數一次">1. 把元件根存成變數一次</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">var</span> <span class="nx">shell</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-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// 之後所有 query 都從 shell 開始
</span></span></span></code></pre></div><p>避免每次 query 都重新從 document 找元件根 — 一是效能（小）、二是 query 範圍仍維持在 shell 內。</p>
<h3 id="2-用-closest-反向找根">2. 用 closest 反向找根</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">getShell</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">return</span> <span class="nx">el</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">getShell</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="c1">// 在這個 shell 內處理
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>事件委派 + closest 適合「多元件實例 + 動態事件處理」 — 各 shell 不需要各自綁 listener、共用一個 listener 用 closest 區分。</p>
<h3 id="3-起點不存在時提早-return">3. 起點不存在時提早 return</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">var</span> <span class="nx">shell</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-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span></span></span></code></pre></div><p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會失敗。提早 return 比後續一連串 null check 乾淨。</p>
<h3 id="4-weakmap-替代-attribute-標記">4. WeakMap 替代 attribute 標記</h3>
<p>當不想污染 DOM attribute 時、用 WeakMap 紀錄已處理的元素：</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="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</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="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</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">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>WeakMap 在元素 GC 時自動清理、不留下 DOM 痕跡。適合短生命週期的 idempotency。</p>
<hr>
<h2 id="設計取捨起點選擇">設計取捨：起點選擇</h2>
<p>Selector 的「起點」有四種做法、各自機會成本不同。這個專案選 B（元件根存變數）當預設、其他做法在特定情境也合理。每張卡片獨立展開該做法的設計細節。</p>
<h3 id="adocumentqueryselector">A：<a href="../pattern-document-query/"><code>document.querySelector</code> 全文件搜</a></h3>
<ul>
<li><strong>機制</strong>：每處 query 都從 document 開始、靠 class name 唯一性命中目標</li>
<li><strong>適合</strong>：原型階段、demo 程式碼、確定全頁只有一個目標且未來不會變</li>
<li><strong>代價</strong>：未來頁面結構變動（加同類 widget、加 demo 區塊）就壞、且失敗模式是安靜地操作錯元素、不報錯</li>
<li><strong>選 A 的時機</strong>：「快速看會不會動」的探索期</li>
</ul>
<h3 id="b元件根存變數之後從變數-query這個專案的預設">B：<a href="../pattern-component-root/">元件根存變數、之後從變數 query</a>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>var shell = document.querySelector('.search-shell')</code> 一次、之後所有 query 用 <code>shell.querySelector(...)</code></li>
<li><strong>選 B 的理由</strong>：當前頁面只有一個 shell、未來可能加（站內搜尋 widget、相關搜尋）— 用變數隔離成本低、提早預防</li>
<li><strong>適合</strong>：一般客製情境、預期未來結構可能擴展</li>
<li><strong>代價</strong>：多一個變數、多一次 query;函式內邏輯變得依賴外部變數</li>
</ul>
<h3 id="c函式接受元件根當參數">C：<a href="../pattern-root-as-parameter/">函式接受元件根當參數</a></h3>
<ul>
<li><strong>機制</strong>：<code>function setup(shell) { shell.querySelector(...) }</code>、外部呼叫 <code>document.querySelectorAll('.shell').forEach(setup)</code></li>
<li><strong>跟 B 的取捨</strong>：B 假設只有一個 shell、C 直接支援多 shell；C 的設計成本前期較高（每函式多一個參數）、但多實例支援是免費的</li>
<li><strong>C 比 B 好的情境</strong>：頁面同時有多個 shell（例如多語切換頁面）、或計劃中要重用組件到不同頁面</li>
</ul>
<h3 id="d事件-">D：<a href="../pattern-closest-lookup/">事件 + <code>closest</code> 反向找根</a></h3>
<ul>
<li><strong>機制</strong>：監聽全域事件、事件處理時 <code>e.target.closest('.shell')</code> 反向找元件根</li>
<li><strong>跟 B/C 的取捨</strong>：B/C 是「初始化時綁定」、D 是「事件發生時動態判斷」— D 適合元件動態出現 / 消失（SPA 路由切換、AJAX 注入）</li>
<li><strong>D 比 C 好的情境</strong>：元件實例在 runtime 動態增減、用 mutation observer 補打成本反而更高</li>
<li><strong>代價</strong>：事件委派的調試比直接綁定難（不知道事件實際從哪傳上來）</li>
</ul>
<hr>
<h2 id="設計取捨範圍深度">設計取捨：範圍深度</h2>
<p><code>querySelector</code> 預設找任意深度、可以收緊到直接子。三種做法：</p>
<h3 id="a任意深度這個專案的預設">A：任意深度（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>shell.querySelector('.target')</code> — 子樹任何深度都接受</li>
<li><strong>選 A 的理由</strong>：結構可能因 framework 升級微調、容忍微調換取維護彈性</li>
<li><strong>代價</strong>：深層結構意外多出同名元素時可能誤命中</li>
</ul>
<h3 id="b直接子-scope--x">B：直接子 <code>:scope &gt; X</code></h3>
<ul>
<li><strong>機制</strong>：<code>shell.querySelector(':scope &gt; .target')</code> — 只找直接子</li>
<li><strong>跟 A 的取捨</strong>：A 容忍結構微調、B 強制結構穩定 — B 帶來「結構變動立即報錯」的早期偵測</li>
<li><strong>B 比 A 好的情境</strong>：自家完全控制的結構、想用 selector 失敗當回歸測試訊號</li>
</ul>
<h3 id="c特定路徑-scope--a--b">C：特定路徑 <code>:scope &gt; A &gt; B</code></h3>
<ul>
<li><strong>機制</strong>：強制一條精確路徑</li>
<li><strong>代價</strong>：結構任何微調都壞、維護成本高</li>
<li><strong>C 才合理的情境</strong>：寫整合測試的結構斷言、不是 production query</li>
</ul>
<hr>
<h2 id="設計取捨過濾與-idempotency">設計取捨：過濾與 idempotency</h2>
<p>apply 函式可能被多次觸發（observer / event / 初始化）、過濾保證每元素只處理一次。三種做法：</p>
<h3 id="adom-attribute-標記這個專案的預設">A：<a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>:not([data-scoped])</code> 過濾 + 處理後 <code>el.setAttribute('data-scoped', 'true')</code></li>
<li><strong>選 A 的理由</strong>：標記跟著 DOM 元素走、元素被移除時自動清理；標記在 devtools 可見、debug 直接</li>
<li><strong>代價</strong>：DOM 上多了一個自家用的 attribute（命名衝突風險小）</li>
</ul>
<h3 id="bweakmap-紀錄">B：<a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></h3>
<ul>
<li><strong>機制</strong>：<code>var processed = new WeakMap(); processed.set(el, true)</code></li>
<li><strong>跟 A 的取捨</strong>：B 不污染 DOM、適合「不想留 attribute 痕跡」的場景；A 在 devtools 可見、debug 較直接</li>
<li><strong>B 比 A 好的情境</strong>：寫成第三方函式庫、不想對使用者 DOM 加屬性</li>
</ul>
<h3 id="c依賴外部呼叫者保證只呼叫一次">C：依賴外部呼叫者保證只呼叫一次</h3>
<ul>
<li><strong>機制</strong>：apply 內不防護、依賴 init 時只綁一次 listener</li>
<li><strong>成本特別高的原因</strong>：observer 觸發 / 事件觸發 / 初始化任一處多呼叫、就產生重複處理 bug；錯誤難以追蹤</li>
<li><strong>C 才合理的情境</strong>：apply 本身是 idempotent 的（例如 set class 設成已是的值、無副作用）— 此時不需過濾</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Selector 精準度問題</th>
          <th>修正動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多處 <code>document.querySelector</code> 同類元素</td>
          <td>起點太寬</td>
          <td>把元件根存變數、之後 query 從變數開始</td>
      </tr>
      <tr>
          <td>同頁加第二個元件實例後行為錯亂</td>
          <td>起點 hardcode</td>
          <td>改「起點當參數」pattern</td>
      </tr>
      <tr>
          <td>Selector 命中了不該命中的元素</td>
          <td>範圍 / 過濾不足</td>
          <td>加 ancestor scope、或加 attribute filter</td>
      </tr>
      <tr>
          <td>Apply 被多次呼叫產生重複處理</td>
          <td>沒 idempotency 防線</td>
          <td>加 <code>:not([data-flag])</code> + 處理後標記</td>
      </tr>
      <tr>
          <td>結構微調後 selector 失效</td>
          <td><code>:scope &gt;</code> 用得太死</td>
          <td>換成任意深度（預設）</td>
      </tr>
      <tr>
          <td>事件處理時不知是哪個元件實例</td>
          <td>沒反向找根機制</td>
          <td>用 <code>closest</code></td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Selector 精準度不是極致最佳化、是 sanity 防線。三維度（起點 / 範圍 / 過濾）一起設計、每個維度都顯式選擇 — 比從寬泛開始一路追 bug 容易得多。</p>
<p>寬 selector（<code>querySelectorAll('.title')</code>）是「便利位置」、窄 selector 是「對齊位置」 — 這個反相關的更高層原則見 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。</p>
]]></content:encoded></item><item><title>用前端測試把排版問題自動化</title><link>https://tarrragon.github.io/blog/report/layout-tests-with-playwright/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/layout-tests-with-playwright/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>當一個版型被 debug 兩次以上、就值得寫成 playwright 測試。&lt;/strong> 測試替代「手動檢查 + 截圖」的循環、讓版型回歸可被機器發現。下次有人改 CSS 時、測試會立刻指出哪個假設被破壞。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>邊界條件多（viewport、字型、互動狀態）&lt;/td>
 &lt;td>人眼難以涵蓋全部組合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>變動觸發點不明顯（改 token、改 theme）&lt;/td>
 &lt;td>改一處不知道哪裡會壞&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>視覺問題往往來自相對關係&lt;/td>
 &lt;td>截圖只看絕對位置、看不出關係&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>人腦適合「驚訝時注意」、不適合「重複檢查 100 個 case 是否如預期」。後者是機器擅長的。&lt;/p>
&lt;h3 id="兩種測試層次">兩種測試層次&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層次&lt;/th>
 &lt;th>測什麼&lt;/th>
 &lt;th>工具&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>視覺迴歸&lt;/td>
 &lt;td>整頁與基準截圖比對&lt;/td>
 &lt;td>Percy / Chromatic / Playwright snapshot&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結構斷言&lt;/td>
 &lt;td>特定元素的位置 / 尺寸 / 順序&lt;/td>
 &lt;td>Playwright &lt;code>browser_evaluate&lt;/code> + &lt;code>expect&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩種互補。視覺迴歸抓「整頁有沒有變」、結構斷言抓「特定關係有沒有保持」。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的測試機會">這次任務的測試機會&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁的版型在這次開發中被 debug 多輪：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Debug 次數&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Filter sidebar 跨 viewport 顯示 / 隱藏&lt;/td>
 &lt;td>5+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope UI 三狀態下的位置&lt;/td>
 &lt;td>4+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結果區跟 sidebar 頂端對齊&lt;/td>
 &lt;td>3+&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter 順序 type 在前&lt;/td>
 &lt;td>2&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>每個 debug 過 ≥ 2 次的版型場景都值得寫測試 — 表示這個地方很容易壞、未來改 CSS 還會踩。&lt;/p>
&lt;h3 id="執行寫-playwright-測試">執行：寫 playwright 測試&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// tests/search-layout.spec.js
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">test&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">expect&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;@playwright/test&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nx">test&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">describe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;search page layout&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;desktop ≥ 1400 顯示左側 filter sidebar&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setViewportSize&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1440&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">900&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/blog/search/&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;pre&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">waitForSelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-panel&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">slot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">$&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">isVisible&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">slot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">isVisible&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">isVisible&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">filterParent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">evaluate&lt;/span>&lt;span class="p">(()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-panel&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">parentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterParent&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toContain&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;viewport &amp;lt; 1400 filter 留在 pagefind drawer&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setViewportSize&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">900&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/blog/search/&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;pre&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">waitForSelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-panel&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">filterParent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">evaluate&lt;/span>&lt;span class="p">(()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-panel&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">parentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterParent&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toContain&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">32&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;scope UI 在三互動狀態下都在 input 與 results 之間&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">({&lt;/span> &lt;span class="nx">page&lt;/span> &lt;span class="p">})&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setViewportSize&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">1440&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">height&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">900&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">35&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="kr">goto&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/blog/search/&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">selector&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">38&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">evaluate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">s&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">getBoundingClientRect&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">y&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">selector&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">39&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 狀態 1：初始載入
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">let&lt;/span> &lt;span class="nx">scopeY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">inputY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">44&lt;/span>&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeY&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBeGreaterThan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">inputY&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">45&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">46&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 狀態 2：點 input
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">47&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">click&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">48&lt;/span>&lt;span class="cl"> &lt;span class="nx">scopeY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl"> &lt;span class="nx">inputY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">50&lt;/span>&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeY&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBeGreaterThan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">inputY&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">51&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">52&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 狀態 3：輸入字
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">53&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">fill&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;pre&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">54&lt;/span>&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">page&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">waitForSelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__results .pagefind-ui__result&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">55&lt;/span>&lt;span class="cl"> &lt;span class="nx">scopeY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">56&lt;/span>&lt;span class="cl"> &lt;span class="nx">inputY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">57&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">resultsY&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">getY&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__results&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">58&lt;/span>&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeY&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBeGreaterThan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">inputY&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">59&lt;/span>&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeY&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBeLessThan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">resultsY&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">60&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">61&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個 &lt;code>expect&lt;/code> 對應一條版型契約 — 這條被破壞時測試紅、改 CSS 的人立刻知道。&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>Playwright 測試&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>高 — 跑所有 case&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規範化&lt;/td>
 &lt;td>否 — 知識在腦中&lt;/td>
 &lt;td>是 — 寫成可讀的 expect&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>第 1 次寫成本中、第 2 次以後成本碾壓手動。&lt;strong>門檻在「會 debug 第 2 次嗎」&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>當一個版型被 debug 兩次以上、就值得寫成 playwright 測試。</strong> 測試替代「手動檢查 + 截圖」的循環、讓版型回歸可被機器發現。下次有人改 CSS 時、測試會立刻指出哪個假設被破壞。</p>
<hr>
<h2 id="為什麼版型問題適合自動化">為什麼版型問題適合自動化</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>排版問題的特徵：</p>
<table>
  <thead>
      <tr>
          <th>特徵</th>
          <th>對手動檢查的不利</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>邊界條件多（viewport、字型、互動狀態）</td>
          <td>人眼難以涵蓋全部組合</td>
      </tr>
      <tr>
          <td>變動觸發點不明顯（改 token、改 theme）</td>
          <td>改一處不知道哪裡會壞</td>
      </tr>
      <tr>
          <td>視覺問題往往來自相對關係</td>
          <td>截圖只看絕對位置、看不出關係</td>
      </tr>
  </tbody>
</table>
<p>人腦適合「驚訝時注意」、不適合「重複檢查 100 個 case 是否如預期」。後者是機器擅長的。</p>
<h3 id="兩種測試層次">兩種測試層次</h3>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>測什麼</th>
          <th>工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視覺迴歸</td>
          <td>整頁與基準截圖比對</td>
          <td>Percy / Chromatic / Playwright snapshot</td>
      </tr>
      <tr>
          <td>結構斷言</td>
          <td>特定元素的位置 / 尺寸 / 順序</td>
          <td>Playwright <code>browser_evaluate</code> + <code>expect</code></td>
      </tr>
  </tbody>
</table>
<p>兩種互補。視覺迴歸抓「整頁有沒有變」、結構斷言抓「特定關係有沒有保持」。</p>
<hr>
<h2 id="這次任務的測試機會">這次任務的測試機會</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁的版型在這次開發中被 debug 多輪：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Debug 次數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter sidebar 跨 viewport 顯示 / 隱藏</td>
          <td>5+</td>
      </tr>
      <tr>
          <td>Scope UI 三狀態下的位置</td>
          <td>4+</td>
      </tr>
      <tr>
          <td>結果區跟 sidebar 頂端對齊</td>
          <td>3+</td>
      </tr>
      <tr>
          <td>Filter 順序 type 在前</td>
          <td>2</td>
      </tr>
  </tbody>
</table>
<h3 id="判讀">判讀</h3>
<p>每個 debug 過 ≥ 2 次的版型場景都值得寫測試 — 表示這個地方很容易壞、未來改 CSS 還會踩。</p>
<h3 id="執行寫-playwright-測試">執行：寫 playwright 測試</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="c1">// tests/search-layout.spec.js
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">test</span><span class="p">,</span> <span class="nx">expect</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;@playwright/test&#39;</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="nx">test</span><span class="p">.</span><span class="nx">describe</span><span class="p">(</span><span class="s1">&#39;search page layout&#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"> 5</span><span class="cl">  <span class="nx">test</span><span class="p">(</span><span class="s1">&#39;desktop ≥ 1400 顯示左側 filter sidebar&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">setViewportSize</span><span class="p">({</span> <span class="nx">width</span><span class="o">:</span> <span class="mi">1440</span><span class="p">,</span> <span class="nx">height</span><span class="o">:</span> <span class="mi">900</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">&#39;/blog/search/&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">fill</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">,</span> <span class="s1">&#39;pre&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kr">const</span> <span class="nx">slot</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="kr">const</span> <span class="nx">isVisible</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">slot</span><span class="p">.</span><span class="nx">isVisible</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">isVisible</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="kr">const</span> <span class="nx">filterParent</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(()</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">).</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">className</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">filterParent</span><span class="p">).</span><span class="nx">toContain</span><span class="p">(</span><span class="s1">&#39;search-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="nx">test</span><span class="p">(</span><span class="s1">&#39;viewport &lt; 1400 filter 留在 pagefind drawer&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">setViewportSize</span><span class="p">({</span> <span class="nx">width</span><span class="o">:</span> <span class="mi">1024</span><span class="p">,</span> <span class="nx">height</span><span class="o">:</span> <span class="mi">900</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">&#39;/blog/search/&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">fill</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">,</span> <span class="s1">&#39;pre&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="kr">const</span> <span class="nx">filterParent</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(()</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">      <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">).</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">className</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">filterParent</span><span class="p">).</span><span class="nx">toContain</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">31</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="nx">test</span><span class="p">(</span><span class="s1">&#39;scope UI 在三互動狀態下都在 input 與 results 之間&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">({</span> <span class="nx">page</span> <span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">setViewportSize</span><span class="p">({</span> <span class="nx">width</span><span class="o">:</span> <span class="mi">1440</span><span class="p">,</span> <span class="nx">height</span><span class="o">:</span> <span class="mi">900</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">&#39;/blog/search/&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="kr">async</span> <span class="kd">function</span> <span class="nx">getY</span><span class="p">(</span><span class="nx">selector</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">      <span class="k">return</span> <span class="nx">page</span><span class="p">.</span><span class="nx">evaluate</span><span class="p">(</span><span class="nx">s</span> <span class="p">=&gt;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">s</span><span class="p">).</span><span class="nx">getBoundingClientRect</span><span class="p">().</span><span class="nx">y</span><span class="p">,</span> <span class="nx">selector</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl">    <span class="c1">// 狀態 1：初始載入
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="c1"></span>    <span class="kd">let</span> <span class="nx">scopeY</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getY</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">43</span><span class="cl">    <span class="kd">let</span> <span class="nx">inputY</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getY</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">scopeY</span><span class="p">).</span><span class="nx">toBeGreaterThan</span><span class="p">(</span><span class="nx">inputY</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="c1">// 狀態 2：點 input
</span></span></span><span class="line"><span class="ln">47</span><span class="cl"><span class="c1"></span>    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="nx">scopeY</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getY</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">49</span><span class="cl">    <span class="nx">inputY</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getY</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">scopeY</span><span class="p">).</span><span class="nx">toBeGreaterThan</span><span class="p">(</span><span class="nx">inputY</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">
</span></span><span class="line"><span class="ln">52</span><span class="cl">    <span class="c1">// 狀態 3：輸入字
</span></span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="c1"></span>    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">fill</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">,</span> <span class="s1">&#39;pre&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">    <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">waitForSelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__results .pagefind-ui__result&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">    <span class="nx">scopeY</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getY</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">56</span><span class="cl">    <span class="nx">inputY</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getY</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">    <span class="kr">const</span> <span class="nx">resultsY</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">getY</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__results&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">scopeY</span><span class="p">).</span><span class="nx">toBeGreaterThan</span><span class="p">(</span><span class="nx">inputY</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">scopeY</span><span class="p">).</span><span class="nx">toBeLessThan</span><span class="p">(</span><span class="nx">resultsY</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>每個 <code>expect</code> 對應一條版型契約 — 這條被破壞時測試紅、改 CSS 的人立刻知道。</p>
<hr>
<h2 id="測試的維護成本與收益">測試的維護成本與收益</h2>
<h3 id="內在屬性比較">內在屬性比較</h3>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>手動檢查</th>
          <th>Playwright 測試</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>高 — 跑所有 case</td>
      </tr>
      <tr>
          <td>規範化</td>
          <td>否 — 知識在腦中</td>
          <td>是 — 寫成可讀的 expect</td>
      </tr>
      <tr>
          <td>教學價值</td>
          <td>低 — 新人需要被告知</td>
          <td>高 — 測試本身是文件</td>
      </tr>
  </tbody>
</table>
<p>第 1 次寫成本中、第 2 次以後成本碾壓手動。<strong>門檻在「會 debug 第 2 次嗎」</strong>。</p>
<hr>
<h2 id="測試什麼不測什麼">測試什麼、不測什麼</h2>
<h3 id="適合測試的版型場景">適合測試的版型場景</h3>
<ul>
<li>跨 viewport 的元件顯示 / 隱藏切換</li>
<li>元件相對位置（A 在 B 上方 / 下方 / 左右）</li>
<li>元件順序（type 在 tag 前）</li>
<li>互動狀態下的位置不變（scope 在三狀態下都在 input 與 results 之間）</li>
</ul>
<h3 id="不適合用-playwright-測">不適合用 playwright 測</h3>
<ul>
<li>純視覺差異（顏色微差、圓角 1px 差） — 用 visual regression 工具</li>
<li>動畫過程 — 不穩定、容易 flaky</li>
<li>字型 rendering 細節 — 跨 OS / 瀏覽器差異大</li>
</ul>
<p>選擇原則：<strong>測「結構性契約」、不測「畫素」</strong>。畫素級檢查交給 visual regression。</p>
<hr>
<h2 id="設計取捨版型驗證機制的選擇">設計取捨：版型驗證機制的選擇</h2>
<p>四種做法、各自機會成本不同。這個專案在版型 debug ≥ 2 次後選 A（結構斷言測試）當預設、其他做法在特定情境合理。</p>
<blockquote>
<p>本篇是 <a href="../two-occurrence-threshold/">#42 2 次門檻</a> 抽象原則在「驗證機制升級」這個面向的應用。</p></blockquote>
<h3 id="aplaywright-結構斷言測試這個專案的預設">A：Playwright 結構斷言測試（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：寫 <code>expect(scopeY &gt; inputY)</code> 這類斷言、自動跑、跨字型 / 主題都對</li>
<li><strong>選 A 的理由</strong>：規範化（測試本身是文件）、跨環境穩定、回歸自動偵測</li>
<li><strong>適合</strong>：debug ≥ 2 次的版型場景、需要長期保護的 layout 契約</li>
<li><strong>代價</strong>：寫測試的初始成本、需要 playwright runtime</li>
</ul>
<h3 id="b手動截圖檢查">B：手動截圖檢查</h3>
<ul>
<li><strong>機制</strong>：開頁面、看截圖、人眼確認</li>
<li><strong>跟 A 的取捨</strong>：B 起步成本 0、A 起步成本中；但 B 重複成本高（每次回歸都要看）</li>
<li><strong>B 比 A 好的情境</strong>：第 1 次驗證（debug 過 1 次、不確定值不值得寫測試）、純探索期</li>
</ul>
<h3 id="cvisual-regression-snapshot">C：Visual regression snapshot</h3>
<ul>
<li><strong>機制</strong>：截整頁圖跟 baseline 比對、像素級差異</li>
<li><strong>跟 A 的取捨</strong>：C 涵蓋率廣（整頁所有變動都偵測）、A 只測指定契約；但 C false positive 多（字型微調 / theme 換色都觸發）</li>
<li><strong>C 比 A 好的情境</strong>：純視覺驗證（marketing page）、設計穩定不常改</li>
</ul>
<h3 id="d不寫測試">D：不寫測試</h3>
<ul>
<li><strong>機制</strong>：純信任手動驗證</li>
<li><strong>跟 A 的取捨</strong>：D 0 維護成本、A 有測試維護；但 D 在版型反覆壞時累積「腦中知識」、新人接手不知道</li>
<li><strong>D 才合理的情境</strong>：純探索期 / prototype、確定不上 production</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該寫測試的時機</th>
          <th>第一個該寫的 expect</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一個版型 bug 出現第 2 次</td>
          <td>立刻寫</td>
          <td>把當時的 fix 寫成 expect</td>
      </tr>
      <tr>
          <td>改 token / theme 時不確定哪些頁面會壞</td>
          <td>把對 token 敏感的頁面寫測試</td>
          <td>元件相對位置、寬度比例</td>
      </tr>
      <tr>
          <td>跨 viewport 的響應式邏輯複雜</td>
          <td>寫 viewport 切換測試</td>
          <td>不同寬度下元件顯示 / 位置</td>
      </tr>
      <tr>
          <td>互動狀態下版型不穩定</td>
          <td>寫狀態切換測試</td>
          <td>各狀態下關鍵元素的位置關係</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：版型契約用測試固定 — 測試紅了表示契約被打破、不是測試壞了。每個紅色測試都是有人改了不該改的東西的訊號。</p>
<p>跟 <a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a> 的關係：layout test 是 Checkpoint 3「Ship 前」的具體做法 — 跨 viewport / 跨狀態 / 跨資料規模驗收、catch 開發中 checkpoint 看不到的整合錯。沒寫 layout test = 把 ship 前 checkpoint 跳過、所有版型回歸都進 ship 後（使用者反映才修）。</p>
<p>寫完 layout test 必須在「未修版型」跑 RED 確認測試會抓到該抓的、再在「修後版型」跑 GREEN 確認修對了 — 兩個訊號都看到、測試才被驗證。詳見 <a href="../test-first-red-before-green/">#69 Test-First：先看到 RED 才相信 GREEN</a>。</p>
]]></content:encoded></item><item><title>空間 / 尺寸類指令的澄清時機</title><link>https://tarrragon.github.io/blog/report/spatial-instruction-clarification/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/spatial-instruction-clarification/</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;strong>關係性指令&lt;/strong> — 描述了兩個元素的相對位置，但沒給絕對數值。實作時需要把關係轉成具體數字（padding 多少、top 多少）。&lt;/p>
&lt;p>執行者依憑感覺取數字 = 用自己的估算代替使用者的意圖。估算與意圖不一致時、使用者看到結果才知道、糾正成本高。&lt;/p>
&lt;p>把計算過程列出來確認 = 把「估算」變成「協商」 — 使用者有機會在實作前指正。&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>Filter 的 padding 對齊&lt;/td>
 &lt;td>「padding 對齊到搜尋欄下緣」&lt;/td>
 &lt;td>估算 padding-top: 152px 寫死&lt;/td>
 &lt;td>視覺差 ~10px、要重做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Breakpoint 取值&lt;/td>
 &lt;td>「桌面顯示左欄、手機隱藏」&lt;/td>
 &lt;td>取 768px 寫死&lt;/td>
 &lt;td>視窗 1200 時看不到 filter、要重做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter 寬度&lt;/td>
 &lt;td>「左欄 400px」&lt;/td>
 &lt;td>直接用 400&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;h3 id="執行澄清-protocol">執行：澄清 protocol&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;/td>
 &lt;td>是 → 直接用；否 → 進下一步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>我能依據已知元件尺寸推算嗎？&lt;/td>
 &lt;td>是 → 列出計算過程確認；否 → 直接問&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推算依賴的假設明確嗎？&lt;/td>
 &lt;td>是 → 列出假設與數字；否 → 列出選項供選擇&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>例：&lt;/p>
&lt;blockquote>
&lt;p>「padding 對齊到搜尋欄下緣 — 我會用 H1 64px + form 68px = 132px 當基準、加 8px buffer 共 140px、OK 嗎？或是你想要其他數字？」&lt;/p>&lt;/blockquote>
&lt;p>把計算過程攤開、使用者可以指正「H1 沒這麼高、應該 70px」、避免實作後重做。&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>100%&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>中 — 一兩輪&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;/tbody>
&lt;/table>
&lt;p>優先順序：&lt;strong>有數值用、可算就算、估算寫死最後選&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="列計算的好習慣">列計算的好習慣&lt;/h2>
&lt;h3 id="1-攤開假設">1. 攤開假設&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">計算 filter padding-top：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> H1 height = 64px（假設、實際看 theme 渲染可能不同）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> Form height = 68px（pagefind input 64 + border 4，鎖定 scale=1.0）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> Gap = 20px（pagefind drawer 預設 margin-top）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> Total = 152px&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個值來源寫清楚 — 使用者可以指出某個假設錯了。&lt;/p>
&lt;h3 id="2-用變數表達">2. 用變數表達&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">padding-top: calc(var(--search-title-h) + var(--search-form-h) + var(--search-gap));&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用變數取代 magic number — 改一處全部跟著動、未來調整成本低。&lt;/p>
&lt;h3 id="3-區分設計值與量測值">3. 區分「設計值」與「量測值」&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>值類型&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>設計值&lt;/td>
 &lt;td>設計師決定的固定值&lt;/td>
 &lt;td>filter 寬度 400px&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>量測值&lt;/td>
 &lt;td>runtime 才知道&lt;/td>
 &lt;td>scope UI 高度（受字型換行）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推算值&lt;/td>
 &lt;td>由其他值計算&lt;/td>
 &lt;td>filter padding = title + form + gap&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>確認時把每個值的類型寫清楚、使用者知道哪些是「我可以決定」、哪些是「實作時才知道」。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>空間 / 尺寸類指令缺少明確數值時、先列計算過程或假設、確認後再實作。</strong> 直接用估算寫死會在使用者實際看到結果時才被發現不對、引起多輪試錯。澄清的成本是「多打一句話」、收益是「一次到位」。</p>
<hr>
<h2 id="為什麼空間指令需要澄清">為什麼空間指令需要澄清</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>「對齊」「擺在旁邊」這類描述是<strong>關係性指令</strong> — 描述了兩個元素的相對位置，但沒給絕對數值。實作時需要把關係轉成具體數字（padding 多少、top 多少）。</p>
<p>執行者依憑感覺取數字 = 用自己的估算代替使用者的意圖。估算與意圖不一致時、使用者看到結果才知道、糾正成本高。</p>
<p>把計算過程列出來確認 = 把「估算」變成「協商」 — 使用者有機會在實作前指正。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>指令</th>
          <th>我的處理</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter 的 padding 對齊</td>
          <td>「padding 對齊到搜尋欄下緣」</td>
          <td>估算 padding-top: 152px 寫死</td>
          <td>視覺差 ~10px、要重做</td>
      </tr>
      <tr>
          <td>Breakpoint 取值</td>
          <td>「桌面顯示左欄、手機隱藏」</td>
          <td>取 768px 寫死</td>
          <td>視窗 1200 時看不到 filter、要重做</td>
      </tr>
      <tr>
          <td>Filter 寬度</td>
          <td>「左欄 400px」</td>
          <td>直接用 400</td>
          <td>一次到位</td>
      </tr>
  </tbody>
</table>
<p>第三個是少數有明確數值的情境、那次一次到位。前兩個沒給數字、靠估算、結果都要重做。</p>
<h3 id="判讀">判讀</h3>
<p>差別在「使用者有沒有給確定值」。沒給時、執行者該做的不是「自己決定」、是「把計算列出來確認」。</p>
<h3 id="執行澄清-protocol">執行：澄清 protocol</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>
  </tbody>
</table>
<p>例：</p>
<blockquote>
<p>「padding 對齊到搜尋欄下緣 — 我會用 H1 64px + form 68px = 132px 當基準、加 8px buffer 共 140px、OK 嗎？或是你想要其他數字？」</p></blockquote>
<p>把計算過程攤開、使用者可以指正「H1 沒這麼高、應該 70px」、避免實作後重做。</p>
<hr>
<h2 id="內在屬性比較四種空間指令處理方式">內在屬性比較：四種空間指令處理方式</h2>
<table>
  <thead>
      <tr>
          <th>處理方式</th>
          <th>一次到位機率</th>
          <th>重做成本</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>直接照使用者給的數值寫</td>
          <td>100%</td>
          <td>無</td>
          <td>使用者明確給數值</td>
      </tr>
      <tr>
          <td>列計算過程確認後寫</td>
          <td>高</td>
          <td>低</td>
          <td>數值可推算</td>
      </tr>
      <tr>
          <td>用估算值寫、之後再調</td>
          <td>中</td>
          <td>中 — 一兩輪</td>
          <td>視覺差容易調的場景</td>
      </tr>
      <tr>
          <td>不確認直接寫死</td>
          <td>低</td>
          <td>高 — 多輪試錯</td>
          <td>不適用</td>
      </tr>
  </tbody>
</table>
<p>優先順序：<strong>有數值用、可算就算、估算寫死最後選</strong>。</p>
<hr>
<h2 id="列計算的好習慣">列計算的好習慣</h2>
<h3 id="1-攤開假設">1. 攤開假設</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">計算 filter padding-top：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  H1 height = 64px（假設、實際看 theme 渲染可能不同）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  Form height = 68px（pagefind input 64 + border 4，鎖定 scale=1.0）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  Gap = 20px（pagefind drawer 預設 margin-top）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  Total = 152px</span></span></code></pre></div><p>每個值來源寫清楚 — 使用者可以指出某個假設錯了。</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">padding-top: calc(var(--search-title-h) + var(--search-form-h) + var(--search-gap));</span></span></code></pre></div><p>使用變數取代 magic number — 改一處全部跟著動、未來調整成本低。</p>
<h3 id="3-區分設計值與量測值">3. 區分「設計值」與「量測值」</h3>
<table>
  <thead>
      <tr>
          <th>值類型</th>
          <th>來源</th>
          <th>例子</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設計值</td>
          <td>設計師決定的固定值</td>
          <td>filter 寬度 400px</td>
      </tr>
      <tr>
          <td>量測值</td>
          <td>runtime 才知道</td>
          <td>scope UI 高度（受字型換行）</td>
      </tr>
      <tr>
          <td>推算值</td>
          <td>由其他值計算</td>
          <td>filter padding = title + form + gap</td>
      </tr>
  </tbody>
</table>
<p>確認時把每個值的類型寫清楚、使用者知道哪些是「我可以決定」、哪些是「實作時才知道」。</p>
<hr>
<h2 id="設計取捨空間尺寸指令的處理策略">設計取捨：空間/尺寸指令的處理策略</h2>
<p>四種做法、各自機會成本不同。這個專案在缺數值時選 A（列計算過程確認）當預設、其他做法在特定情境合理。</p>
<h3 id="a列計算過程確認後實作這個專案的預設">A：列計算過程確認後實作（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：把計算過程與假設攤開、給使用者確認、再用 <code>calc(var(...) + var(...))</code> 實作</li>
<li><strong>選 A 的理由</strong>：使用者能指正特定假設（不只「對」「不對」）、實作後改 token 自動跟上</li>
<li><strong>適合</strong>：缺數值的空間 / 尺寸指令（大多數情境）</li>
<li><strong>代價</strong>：多一輪對話確認</li>
</ul>
<h3 id="b直接照使用者給的數值寫">B：直接照使用者給的數值寫</h3>
<ul>
<li><strong>機制</strong>：使用者給「filter 寬 400px」就直接 <code>width: 400px</code></li>
<li><strong>跟 A 的取捨</strong>：B 一次到位、無需確認；前提是使用者已給確切數值</li>
<li><strong>B 比 A 好的情境</strong>：使用者明確指定數值、執行者只需照寫</li>
</ul>
<h3 id="c用估算值寫之後再調">C：用估算值寫、之後再調</h3>
<ul>
<li><strong>機制</strong>：執行者依感覺給數字（「padding-top: 152px」）、寫了之後等使用者試</li>
<li><strong>跟 A 的取捨</strong>：C 看似省溝通成本、實際把確認延後到實作後；C 在估算錯時要重做</li>
<li><strong>C 才合理的情境</strong>：視覺差容易調（&lt; 5px 微差）、且確認成本比實作高（罕見）</li>
</ul>
<h3 id="d不確認直接寫死">D：不確認直接寫死</h3>
<ul>
<li><strong>機制</strong>：執行者自己決定數值、不問也不告知</li>
<li><strong>成本特別高的原因</strong>：跟使用者意圖不一致時要重做、多輪試錯</li>
<li><strong>D 是反模式</strong>：「使用者會看到的數字」屬於該確認的決定（<a href="../decide-vs-confirm-boundary/">#21 可決定 vs 該先確認</a>）— 自決會跟意圖不一致、被退回時白做</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該觸發澄清的指令</th>
          <th>第一個該確認的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「對齊到 X」</td>
          <td>對齊基準是 X 的哪個邊（top / bottom / center）？</td>
          <td>基準位置</td>
      </tr>
      <tr>
          <td>「靠在 Y 旁邊」</td>
          <td>多近？同 row 還是 above / below？</td>
          <td>相對方向與距離</td>
      </tr>
      <tr>
          <td>「desktop 顯示、mobile 隱藏」</td>
          <td>Breakpoint 多少 px？</td>
          <td>viewport 切換點</td>
      </tr>
      <tr>
          <td>「跟 Z 一樣高」</td>
          <td>Z 的高度是固定還是動態？</td>
          <td>量測 vs 寫死</td>
      </tr>
      <tr>
          <td>「適中的 padding」</td>
          <td>「適中」對應多少 px？</td>
          <td>給選項供選擇</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：空間關係描述只是意圖、不是實作數字。把意圖翻譯成數字的過程要對外可見、讓使用者參與。</p>
<p>第三輪指令類型現有五類：空間（本卡）/ 相對位置（<a href="../relative-position-instruction-clarification/">#17</a>）/ 隔離（<a href="../isolation-instruction-clarification/">#18</a>）/ 決定權（<a href="../decide-vs-confirm-boundary/">#21</a>）/ 篩選（<a href="../filter-instruction-clarification/">#58</a>）。前四類缺幾何 / 邊界 / 拍板資訊、第 5 類缺操作層級資訊。</p>
]]></content:encoded></item><item><title>元件相對位置類指令的澄清時機</title><link>https://tarrragon.github.io/blog/report/relative-position-instruction-clarification/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/relative-position-instruction-clarification/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>相對位置指令（「X 在 Y 旁邊」「Y 顯示在 main 左側」）是模糊的 — 同一句話可以對應多種 layout 結構。實作前用文字描述意圖中的 layout 草圖、讓使用者確認後再寫 CSS。&lt;/strong> 沒確認直接擺、實作出來常與使用者預期不同、要重做。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼相對位置需要澄清">為什麼相對位置需要澄清&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>「X 在 Y 旁邊」這類描述至少對應四種 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>Y 與 X 同 row、grid / flex 並排&lt;/td>
 &lt;td>並排、Y 影響 X 寬度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>X absolute 浮在 Y 旁邊&lt;/td>
 &lt;td>並排、X 不影響 Y&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Y 與 X 不同層、Y 在 main 內、X 在 main 外&lt;/td>
 &lt;td>跨 main 邊界、視覺感不同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>X fixed 永遠在 viewport 同位置&lt;/td>
 &lt;td>隨捲動固定、Y 隨內容捲動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>執行者選哪種、結果不同。憑直覺選 ≠ 使用者意圖。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「scope UI 在搜尋框附近」&lt;/td>
 &lt;td>放搜尋框上方&lt;/td>
 &lt;td>應該在搜尋框下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「filter 在主欄左側」&lt;/td>
 &lt;td>放進 main 的 grid 左 column&lt;/td>
 &lt;td>是 main 外的 absolute sidebar&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「左右兩欄獨立排版」&lt;/td>
 &lt;td>同 grid container 兩個 column&lt;/td>
 &lt;td>兩個獨立 layer、用 absolute 疊層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個指令都讓我選了使用者沒想要的實作。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>問題在「相對位置」這類描述本質就是模糊的 — 沒指定座標系、沒指定相對層級、沒指定誰是 layout flow 主角。&lt;/p>
&lt;p>執行前該做的：把我準備寫的 layout 用文字描述出來、讓使用者確認意圖一致。&lt;/p>
&lt;h3 id="執行layout-草圖-protocol">執行：layout 草圖 protocol&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"> - main 維持 70ch 置中（layout flow 主角）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> - filter 用 absolute 浮在 main 左外側、寬 400px、間距 2rem
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> - scope UI 在 main 內、緊接 search input 下方、結果上方
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> - 三者位置關係：filter 與 main 同 viewport 中段、scope 在 main 內
&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>把意圖中的 layout 結構攤開 — 使用者可以指正「filter 應該在 grid 內、不是 absolute」、避免實作後重做。&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;/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>加層級指示（「在 main 內」「跨 main 外」）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>縮小範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>加 layout 結構（「同 grid」「absolute 疊層」）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>接近實作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文字草圖 + 確認&lt;/td>
 &lt;td>最低&lt;/td>
 &lt;td>實作前最後確認&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>執行者該主動把對話從「純位置詞」推到「文字草圖 + 確認」 — 縮小模糊範圍。&lt;/p>
&lt;hr>
&lt;h2 id="文字草圖的好習慣">文字草圖的好習慣&lt;/h2>
&lt;h3 id="1-描述-layout-主角">1. 描述 layout 主角&lt;/h3>
&lt;p>哪個元素是「決定其他元素位置的 anchor」？通常是內容主體（main、article）。其他元素相對這個主角描述。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>相對位置指令（「X 在 Y 旁邊」「Y 顯示在 main 左側」）是模糊的 — 同一句話可以對應多種 layout 結構。實作前用文字描述意圖中的 layout 草圖、讓使用者確認後再寫 CSS。</strong> 沒確認直接擺、實作出來常與使用者預期不同、要重做。</p>
<hr>
<h2 id="為什麼相對位置需要澄清">為什麼相對位置需要澄清</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>「X 在 Y 旁邊」這類描述至少對應四種 layout 實作：</p>
<table>
  <thead>
      <tr>
          <th>實作方式</th>
          <th>視覺結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Y 與 X 同 row、grid / flex 並排</td>
          <td>並排、Y 影響 X 寬度</td>
      </tr>
      <tr>
          <td>X absolute 浮在 Y 旁邊</td>
          <td>並排、X 不影響 Y</td>
      </tr>
      <tr>
          <td>Y 與 X 不同層、Y 在 main 內、X 在 main 外</td>
          <td>跨 main 邊界、視覺感不同</td>
      </tr>
      <tr>
          <td>X fixed 永遠在 viewport 同位置</td>
          <td>隨捲動固定、Y 隨內容捲動</td>
      </tr>
  </tbody>
</table>
<p>執行者選哪種、結果不同。憑直覺選 ≠ 使用者意圖。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<table>
  <thead>
      <tr>
          <th>指令</th>
          <th>我的初次理解</th>
          <th>使用者實際意圖</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「scope UI 在搜尋框附近」</td>
          <td>放搜尋框上方</td>
          <td>應該在搜尋框下方</td>
      </tr>
      <tr>
          <td>「filter 在主欄左側」</td>
          <td>放進 main 的 grid 左 column</td>
          <td>是 main 外的 absolute sidebar</td>
      </tr>
      <tr>
          <td>「左右兩欄獨立排版」</td>
          <td>同 grid container 兩個 column</td>
          <td>兩個獨立 layer、用 absolute 疊層</td>
      </tr>
  </tbody>
</table>
<p>每個指令都讓我選了使用者沒想要的實作。</p>
<h3 id="判讀">判讀</h3>
<p>問題在「相對位置」這類描述本質就是模糊的 — 沒指定座標系、沒指定相對層級、沒指定誰是 layout flow 主角。</p>
<p>執行前該做的：把我準備寫的 layout 用文字描述出來、讓使用者確認意圖一致。</p>
<h3 id="執行layout-草圖-protocol">執行：layout 草圖 protocol</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">  - main 維持 70ch 置中（layout flow 主角）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  - filter 用 absolute 浮在 main 左外側、寬 400px、間距 2rem
</span></span><span class="line"><span class="ln">4</span><span class="cl">  - scope UI 在 main 內、緊接 search input 下方、結果上方
</span></span><span class="line"><span class="ln">5</span><span class="cl">  - 三者位置關係：filter 與 main 同 viewport 中段、scope 在 main 內
</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>把意圖中的 layout 結構攤開 — 使用者可以指正「filter 應該在 grid 內、不是 absolute」、避免實作後重做。</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>加層級指示（「在 main 內」「跨 main 外」）</td>
          <td>中</td>
          <td>縮小範圍</td>
      </tr>
      <tr>
          <td>加 layout 結構（「同 grid」「absolute 疊層」）</td>
          <td>低</td>
          <td>接近實作</td>
      </tr>
      <tr>
          <td>文字草圖 + 確認</td>
          <td>最低</td>
          <td>實作前最後確認</td>
      </tr>
  </tbody>
</table>
<p>執行者該主動把對話從「純位置詞」推到「文字草圖 + 確認」 — 縮小模糊範圍。</p>
<hr>
<h2 id="文字草圖的好習慣">文字草圖的好習慣</h2>
<h3 id="1-描述-layout-主角">1. 描述 layout 主角</h3>
<p>哪個元素是「決定其他元素位置的 anchor」？通常是內容主體（main、article）。其他元素相對這個主角描述。</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">Layout 主角：main（70ch 置中）
</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">  - H1：main 內、第一個元素
</span></span><span class="line"><span class="ln">4</span><span class="cl">  - filter：main 左外側 400px
</span></span><span class="line"><span class="ln">5</span><span class="cl">  - footer：main 之後</span></span></code></pre></div><h3 id="2-描述層級關係">2. 描述層級關係</h3>
<p>哪些元素在同一 layout 流、哪些是疊層？</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">Layout flow 內：H1、search input、results
</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">  - filter：absolute、浮在 main 左外
</span></span><span class="line"><span class="ln">4</span><span class="cl">  - scope UI：absolute、浮在 search input 下方</span></span></code></pre></div><h3 id="3-描述-viewport-行為">3. 描述 viewport 行為</h3>
<p>不同 viewport 下、layout 怎麼變？</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">Desktop ≥ 1400：filter 顯示在左外、scope UI 浮在 input 下方
</span></span><span class="line"><span class="ln">2</span><span class="cl">Mobile &lt; 1400：filter 移到 pagefind drawer 內、scope UI 維持原位</span></span></code></pre></div><hr>
<h2 id="對使用者描述-layout-的格式">對使用者描述 layout 的格式</h2>
<h3 id="inline-ascii-草圖">Inline ASCII 草圖</h3>
<p>文字環境下、ASCII 比 PNG 圖更實用：</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">│            │ H1 搜尋           │
</span></span><span class="line"><span class="ln">3</span><span class="cl">│ filter     ├──────────────────┤
</span></span><span class="line"><span class="ln">4</span><span class="cl">│ (sticky)   │ Search input     │
</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">│            │ Scope UI         │
</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">│            │ Results          │
</span></span><span class="line"><span class="ln">9</span><span class="cl">└────────────┴──────────────────┘</span></span></code></pre></div><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">  1. 左欄：filter（400px、main 外、sticky）
</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">     a. H1 搜尋
</span></span><span class="line"><span class="ln">5</span><span class="cl">     b. Search input（中央欄）
</span></span><span class="line"><span class="ln">6</span><span class="cl">     c. Scope UI（input 下、results 上）
</span></span><span class="line"><span class="ln">7</span><span class="cl">     d. Results</span></span></code></pre></div><p>兩種格式選一種、確認後再寫 CSS。</p>
<hr>
<h2 id="設計取捨相對位置指令的處理策略">設計取捨：相對位置指令的處理策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（文字草圖確認）當預設、其他做法在特定情境合理。</p>
<h3 id="a文字-layout-草圖--主角疊層說明確認這個專案的預設">A：文字 layout 草圖 + 主角/疊層說明確認（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：把意圖翻成「main 70ch 置中、filter absolute 浮在 main 左外、scope 在 main 內 absolute 浮在 input 下」這類具體描述、使用者確認</li>
<li><strong>選 A 的理由</strong>：草圖 30 秒打完、使用者 30 秒內 yes/no、避免寫完 CSS 才被指正</li>
<li><strong>適合</strong>：相對位置指令的多數情境</li>
<li><strong>代價</strong>：多 30 秒對話成本</li>
</ul>
<h3 id="bascii-圖更詳細">B：ASCII 圖更詳細</h3>
<ul>
<li><strong>機制</strong>：用 box-drawing 字元畫實際 layout 草圖</li>
<li><strong>跟 A 的取捨</strong>：B 視覺直觀、A 文字描述更精確；B 對複雜 layout 表達力強</li>
<li><strong>B 比 A 好的情境</strong>：layout 涉及 ≥ 4 個區塊、文字描述容易混淆</li>
</ul>
<h3 id="c加層級指示中間步驟">C：加層級指示（中間步驟）</h3>
<ul>
<li><strong>機制</strong>：「filter 在 main 外的 sidebar」（不是「在 main 旁邊」）</li>
<li><strong>跟 A 的取捨</strong>：C 比純位置詞具體、比草圖簡略；用於初步對話</li>
<li><strong>C 比 A 好的情境</strong>：對話初期、還沒到實作決策的階段</li>
</ul>
<h3 id="d純位置詞直接實作">D：純位置詞直接實作</h3>
<ul>
<li><strong>機制</strong>：照「filter 在 main 旁邊」字面意思挑一種實作</li>
<li><strong>成本特別高的原因</strong>：同一句話對應多種 layout 結構、選錯就重做</li>
<li><strong>D 是反模式</strong>：「相對位置」永遠模糊、跳過確認等於賭 — 選錯重做的成本遠大於確認的對話成本</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該觸發澄清的指令</th>
          <th>第一個該確認的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「X 在 Y 旁邊」</td>
          <td>同 row 還是疊層？X 影響 Y 的位置嗎？</td>
          <td>Layout 結構（grid / absolute）</td>
      </tr>
      <tr>
          <td>「Y 顯示在 main 左側」</td>
          <td>在 main 內的 column 還是 main 外的 sidebar？</td>
          <td>跨界與否</td>
      </tr>
      <tr>
          <td>「左右兩欄獨立」</td>
          <td>「獨立」是 DOM 獨立、layout 獨立、還是 state 獨立？</td>
          <td>隔離程度</td>
      </tr>
      <tr>
          <td>「跟 Z 對齊」</td>
          <td>Z 的哪個邊（top / bottom / left / right）？</td>
          <td>對齊基準</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：相對位置描述只是意圖、不是實作藍圖。實作前先把意圖畫成 layout 草圖、確認後再開工。</p>
<p>第三輪指令類型現有五類：空間（<a href="../spatial-instruction-clarification/">#16</a>）/ 相對位置（本卡）/ 隔離（<a href="../isolation-instruction-clarification/">#18</a>）/ 決定權（<a href="../decide-vs-confirm-boundary/">#21</a>）/ 篩選（<a href="../filter-instruction-clarification/">#58</a>）。</p>
]]></content:encoded></item><item><title>隔離程度類指令的澄清時機</title><link>https://tarrragon.github.io/blog/report/isolation-instruction-clarification/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/isolation-instruction-clarification/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>「隔離」「不要動 X」是描述意圖的詞、不是描述實作的詞。&lt;/strong> 實作前先確認隔離邊界是哪一種：DOM 結構、layout flow、state、framework 管轄區。每種邊界的實作策略不同、混淆會做出不符合使用者意圖的隔離。&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>DOM 結構隔離&lt;/td>
 &lt;td>兩個元件不在同一 DOM 子樹&lt;/td>
 &lt;td>分開放在不同 ancestor&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Layout flow 隔離&lt;/td>
 &lt;td>一個元件的 layout 不影響另一個&lt;/td>
 &lt;td>absolute / fixed 跳出 flow&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State 隔離&lt;/td>
 &lt;td>兩個元件的 state 互不影響&lt;/td>
 &lt;td>各自有獨立 state container&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 管轄區隔離&lt;/td>
 &lt;td>客製 UI 與 framework UI 各自管自己&lt;/td>
 &lt;td>客製 UI 留在 framework 邊界外&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每種需要的實作完全不同。執行者選錯邊界 = 做了不符合使用者意圖的「隔離」。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「不動 pagefind 行為」&lt;/td>
 &lt;td>不改 pagefind 的 search 邏輯&lt;/td>
 &lt;td>包含「不在 pagefind DOM 內塞客製元素」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「左右不要在同一層」&lt;/td>
 &lt;td>DOM 層級分開&lt;/td>
 &lt;td>Layout flow 互不影響（用 absolute 疊層）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「不動 pagefind 行為」這句、我多次把 scope UI 注入 pagefind DOM、違反使用者的隔離意圖 — 我以為「不動」只指邏輯、實際包含 DOM 結構。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>執行前該確認：「不動 / 隔離」的邊界是哪一種類型？&lt;/p>
&lt;h3 id="執行邊界類型確認-protocol">執行：邊界類型確認 protocol&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">你說的「不動 pagefind」涵蓋哪些？
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> □ 不改 pagefind 的搜尋邏輯（不 fork / patch source）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> □ 不改 pagefind 渲染的 DOM 結構（不在內部 appendChild）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> □ 不改 pagefind 的 state（checkbox 勾選等）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> □ 完全不互動（連 CSS 都不覆寫）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用者勾選對應項、執行者照那個邊界實作。&lt;/p>
&lt;p>或更簡單的問法：「具體來說、什麼算動、什麼不算動？」&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>DOM 結構&lt;/td>
 &lt;td>看 ancestor chain 是否相同&lt;/td>
 &lt;td>一個元件可能影響另一個的 layout / 樣式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Layout flow&lt;/td>
 &lt;td>看是否在同一 flow（grid / flex / block）&lt;/td>
 &lt;td>一個元件撐開影響另一個位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State&lt;/td>
 &lt;td>看 state 是否共用 store&lt;/td>
 &lt;td>一個元件改 state 影響另一個顯示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 管轄區&lt;/td>
 &lt;td>看是否在 framework 渲染週期內&lt;/td>
 &lt;td>Framework patch 時客製被清掉&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>執行前確認邊界類型 — 不確定就問。&lt;/p>
&lt;hr>
&lt;h2 id="隔離邊界的辨識技巧">隔離邊界的辨識技巧&lt;/h2>
&lt;h3 id="1-從會發生什麼反推">1. 從「會發生什麼」反推&lt;/h3>
&lt;p>問使用者：「如果 X 改了、Y 應該也跟著改嗎？」&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>是 — Y 跟著動&lt;/td>
 &lt;td>沒有隔離、共用 state&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>否 — Y 不動&lt;/td>
 &lt;td>State 隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>否 — 連 layout 都不變&lt;/td>
 &lt;td>Layout flow 隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>否 — Y 完全感知不到 X&lt;/td>
 &lt;td>DOM 隔離&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="2-從動誰會壞反推">2. 從「動誰會壞」反推&lt;/h3>
&lt;p>問：「我能在哪個 DOM 區域加東西？」&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>「隔離」「不要動 X」是描述意圖的詞、不是描述實作的詞。</strong> 實作前先確認隔離邊界是哪一種：DOM 結構、layout flow、state、framework 管轄區。每種邊界的實作策略不同、混淆會做出不符合使用者意圖的隔離。</p>
<hr>
<h2 id="為什麼隔離需要澄清">為什麼隔離需要澄清</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>「隔離」這個詞涵蓋多種獨立的概念：</p>
<table>
  <thead>
      <tr>
          <th>隔離類型</th>
          <th>含義</th>
          <th>實作策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM 結構隔離</td>
          <td>兩個元件不在同一 DOM 子樹</td>
          <td>分開放在不同 ancestor</td>
      </tr>
      <tr>
          <td>Layout flow 隔離</td>
          <td>一個元件的 layout 不影響另一個</td>
          <td>absolute / fixed 跳出 flow</td>
      </tr>
      <tr>
          <td>State 隔離</td>
          <td>兩個元件的 state 互不影響</td>
          <td>各自有獨立 state container</td>
      </tr>
      <tr>
          <td>Framework 管轄區隔離</td>
          <td>客製 UI 與 framework UI 各自管自己</td>
          <td>客製 UI 留在 framework 邊界外</td>
      </tr>
  </tbody>
</table>
<p>每種需要的實作完全不同。執行者選錯邊界 = 做了不符合使用者意圖的「隔離」。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<table>
  <thead>
      <tr>
          <th>指令</th>
          <th>我的初次理解</th>
          <th>使用者實際意圖</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「不動 pagefind 行為」</td>
          <td>不改 pagefind 的 search 邏輯</td>
          <td>包含「不在 pagefind DOM 內塞客製元素」</td>
      </tr>
      <tr>
          <td>「左右不要在同一層」</td>
          <td>DOM 層級分開</td>
          <td>Layout flow 互不影響（用 absolute 疊層）</td>
      </tr>
  </tbody>
</table>
<p>「不動 pagefind 行為」這句、我多次把 scope UI 注入 pagefind DOM、違反使用者的隔離意圖 — 我以為「不動」只指邏輯、實際包含 DOM 結構。</p>
<h3 id="判讀">判讀</h3>
<p>執行前該確認：「不動 / 隔離」的邊界是哪一種類型？</p>
<h3 id="執行邊界類型確認-protocol">執行：邊界類型確認 protocol</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">你說的「不動 pagefind」涵蓋哪些？
</span></span><span class="line"><span class="ln">2</span><span class="cl">  □ 不改 pagefind 的搜尋邏輯（不 fork / patch source）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  □ 不改 pagefind 渲染的 DOM 結構（不在內部 appendChild）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  □ 不改 pagefind 的 state（checkbox 勾選等）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  □ 完全不互動（連 CSS 都不覆寫）</span></span></code></pre></div><p>使用者勾選對應項、執行者照那個邊界實作。</p>
<p>或更簡單的問法：「具體來說、什麼算動、什麼不算動？」</p>
<hr>
<h2 id="內在屬性比較四種隔離邊界的特徵">內在屬性比較：四種隔離邊界的特徵</h2>
<table>
  <thead>
      <tr>
          <th>邊界類型</th>
          <th>偵測方式</th>
          <th>違反的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM 結構</td>
          <td>看 ancestor chain 是否相同</td>
          <td>一個元件可能影響另一個的 layout / 樣式</td>
      </tr>
      <tr>
          <td>Layout flow</td>
          <td>看是否在同一 flow（grid / flex / block）</td>
          <td>一個元件撐開影響另一個位置</td>
      </tr>
      <tr>
          <td>State</td>
          <td>看 state 是否共用 store</td>
          <td>一個元件改 state 影響另一個顯示</td>
      </tr>
      <tr>
          <td>Framework 管轄區</td>
          <td>看是否在 framework 渲染週期內</td>
          <td>Framework patch 時客製被清掉</td>
      </tr>
  </tbody>
</table>
<p>執行前確認邊界類型 — 不確定就問。</p>
<hr>
<h2 id="隔離邊界的辨識技巧">隔離邊界的辨識技巧</h2>
<h3 id="1-從會發生什麼反推">1. 從「會發生什麼」反推</h3>
<p>問使用者：「如果 X 改了、Y 應該也跟著改嗎？」</p>
<table>
  <thead>
      <tr>
          <th>答案</th>
          <th>對應邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>是 — Y 跟著動</td>
          <td>沒有隔離、共用 state</td>
      </tr>
      <tr>
          <td>否 — Y 不動</td>
          <td>State 隔離</td>
      </tr>
      <tr>
          <td>否 — 連 layout 都不變</td>
          <td>Layout flow 隔離</td>
      </tr>
      <tr>
          <td>否 — Y 完全感知不到 X</td>
          <td>DOM 隔離</td>
      </tr>
  </tbody>
</table>
<h3 id="2-從動誰會壞反推">2. 從「動誰會壞」反推</h3>
<p>問：「我能在哪個 DOM 區域加東西？」</p>
<table>
  <thead>
      <tr>
          <th>答案</th>
          <th>對應邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>任何地方都可以</td>
          <td>沒有隔離</td>
      </tr>
      <tr>
          <td>自家元件內可以、X 元件內不行</td>
          <td>Framework 管轄區隔離</td>
      </tr>
      <tr>
          <td>自家元件內也要小心、不能影響 X 的 layout</td>
          <td>Layout flow 隔離</td>
      </tr>
      <tr>
          <td>完全分開、不能放在同一 DOM 子樹</td>
          <td>DOM 結構隔離</td>
      </tr>
  </tbody>
</table>
<h3 id="3-從升級時誰要動反推">3. 從「升級時誰要動」反推</h3>
<p>問：「X 升級時、我們要回頭改什麼嗎？」</p>
<table>
  <thead>
      <tr>
          <th>答案</th>
          <th>對應邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不用、客製跟 X 完全分開</td>
          <td>DOM + framework 管轄區隔離</td>
      </tr>
      <tr>
          <td>可能要調整客製 CSS</td>
          <td>Layout / state 邊界內客製</td>
      </tr>
      <tr>
          <td>客製要重做</td>
          <td>沒有隔離、客製深入 X 內部</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計取捨隔離指令的處理策略">設計取捨：隔離指令的處理策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（列邊界類型確認）當預設、其他做法在特定情境合理。</p>
<h3 id="a列四種邊界類型確認這個專案的預設">A：列四種邊界類型確認（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：列出 DOM 結構 / layout flow / state / framework 管轄區四選項、使用者勾選對應項</li>
<li><strong>選 A 的理由</strong>：明確 + 完整、使用者快速指出真正想要的隔離</li>
<li><strong>適合</strong>：隔離意圖不明確的情境</li>
<li><strong>代價</strong>：多一段四選項描述</li>
</ul>
<h3 id="b反推法從會發生什麼動誰會壞反推">B：反推法（從「會發生什麼」「動誰會壞」反推）</h3>
<ul>
<li><strong>機制</strong>：問「如果 X 改了、Y 應該也跟著改嗎？」「我能在哪個區域加東西？」根據答案歸類邊界</li>
<li><strong>跟 A 的取捨</strong>：B 對使用者比較不負擔（不需理解四種邊界術語）、A 直接呈現答案空間</li>
<li><strong>B 比 A 好的情境</strong>：使用者不熟悉 DOM / layout / framework 術語</li>
</ul>
<h3 id="c依直覺選一種實作">C：依直覺選一種實作</h3>
<ul>
<li><strong>機制</strong>：執行者自己解讀「隔離」、選一種實作</li>
<li><strong>跟 A/B 的取捨</strong>：C 跳過確認、A/B 確認；C 在意圖一致時省時間、不一致時重做</li>
<li><strong>C 是反模式</strong>：「隔離」這個詞本身就太模糊、跳過確認等於賭 — 意圖不一致時重做的成本遠大於確認的對話成本</li>
</ul>
<h3 id="d通用問法具體什麼算動">D：通用問法「具體什麼算動」</h3>
<ul>
<li><strong>機制</strong>：直接問「具體來說、什麼算動、什麼不算動？」</li>
<li><strong>跟 A 的取捨</strong>：D 比 A 更簡略、把分類負擔丟給使用者；A 直接給選項使用者只需勾選</li>
<li><strong>D 比 A 好的情境</strong>：使用者已經想得很清楚、只需要表達 — A 反而過於 structured</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該觸發澄清的指令</th>
          <th>第一個該確認的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「不要動 X」</td>
          <td>動哪個層面（邏輯 / DOM / state / 樣式）？</td>
          <td>邊界類型</td>
      </tr>
      <tr>
          <td>「X 跟 Y 隔離」</td>
          <td>隔離 DOM、layout、state、還是 framework？</td>
          <td>邊界類型</td>
      </tr>
      <tr>
          <td>「X 跟 Y 獨立」</td>
          <td>獨立指什麼？哪個改不影響另一個？</td>
          <td>哪個維度獨立</td>
      </tr>
      <tr>
          <td>「X 不能影響 Y」</td>
          <td>影響的方式（layout / state / 樣式）？</td>
          <td>影響途徑</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：隔離指令的核心是「隔離邊界類型」 — 這個答案決定實作策略。沒確認邊界就動手 = 隨機選一種隔離、可能與使用者意圖不一致。</p>
<p>第三輪指令類型現有五類：空間（<a href="../spatial-instruction-clarification/">#16</a>）/ 相對位置（<a href="../relative-position-instruction-clarification/">#17</a>）/ 隔離（本卡）/ 決定權（<a href="../decide-vs-confirm-boundary/">#21</a>）/ 篩選（<a href="../filter-instruction-clarification/">#58</a>）。</p>
]]></content:encoded></item><item><title>覆寫深度的成本告知</title><link>https://tarrragon.github.io/blog/report/override-depth-cost-report/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/override-depth-cost-report/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>覆寫成本要在開工前報、不能默默承擔。&lt;/strong> 客製要對抗多層（UA 預設 + 跨瀏覽器 + framework specificity）時、先把「需要寫多少 / 哪幾條 / 是否有殘留風險」攤開、讓使用者用「視覺改善 vs 維護成本」的角度判斷值不值。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>執行者覺得值得、使用者覺得太貴&lt;/td>
 &lt;td>寫了 5 條 CSS 後使用者說「先還原」、白做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>執行者覺得不值、使用者覺得很重要&lt;/td>
 &lt;td>沒做、使用者覺得敷衍&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩種失敗都來自「成本與價值的判斷沒對齊」。把成本攤開讓使用者參與判斷、避免兩種失敗。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要移除 Pagefind filter 的 &lt;code>&amp;lt;details&amp;gt;&amp;lt;summary&amp;gt;&lt;/code> disclosure 三角圖示。&lt;/p>
&lt;p>我嘗試了：&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">/* 1. UA 預設 marker（Chrome / Firefox） */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nt">summary&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="nd">marker&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">content&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">color&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">transparent&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">font-size&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&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="c">/* 2. UA 預設 marker（Safari / 老 Chrome） */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="nt">summary&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="nd">-webkit-details-marker&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&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"> 5&lt;/span>&lt;span class="cl">&lt;span class="c">/* 3. 改 summary display 避免 list-item 行為 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nt">summary&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">block&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">list-style&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&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"> 7&lt;/span>&lt;span class="cl">&lt;span class="c">/* 4. 蓋過 pagefind 的 ::after 自訂 chevron */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-name&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="nd">after&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&lt;/span> &lt;span class="cp">!important&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"> 9&lt;/span>&lt;span class="cl">&lt;span class="c">/* 5. 處理 reset 邊界外的 fallback */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c">/* ... */&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寫完後使用者說：「如果要做到這麼大的複寫原設計、先還原回去、先不要做這個變更」。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>我的失敗：開工前沒報「這個客製需要對抗 UA + 跨瀏覽器 + framework 三層、預估 5+ 條 CSS、跨瀏覽器有殘留風險」。直接做、使用者看到改動規模才退回。&lt;/p>
&lt;p>正確流程應該在嘗試第 2 條 CSS 時就停下來報告：&lt;/p>
&lt;blockquote>
&lt;p>移除 disclosure 三角需要對抗：&lt;/p>
&lt;ul>
&lt;li>UA 預設 marker（Chrome / Firefox 的 &lt;code>::marker&lt;/code>、Safari / 老 Chrome 的 &lt;code>::-webkit-details-marker&lt;/code>）&lt;/li>
&lt;li>Summary 預設 display: list-item 行為&lt;/li>
&lt;li>Pagefind 的自訂 &lt;code>::after&lt;/code> chevron&lt;/li>
&lt;/ul>
&lt;p>預估 4-5 條 CSS 跨 3 種瀏覽器寫法、可能有殘留風險（&lt;code>::marker&lt;/code> 在某些瀏覽器版本不能用 display: none）。&lt;/p>
&lt;p>視覺改善：filter 名稱旁邊少一個小三角。&lt;/p>
&lt;p>值得做嗎？&lt;/p>&lt;/blockquote>
&lt;p>讓使用者判斷再決定。&lt;/p>
&lt;h3 id="執行覆寫成本告知-protocol">執行：覆寫成本告知 protocol&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>UA 預設&lt;/td>
 &lt;td>跨瀏覽器有差異嗎？需要寫幾種 pseudo？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework specificity&lt;/td>
 &lt;td>框架 CSS 用 hash class 提升 specificity 嗎？需要 layers / important / 雙寫嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 渲染週期&lt;/td>
 &lt;td>改了會被 reset 嗎？需要 observer 補打嗎？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>任一層有「需要對抗」就把成本報出來、讓使用者參與決定。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1 條 CSS、單一瀏覽器&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>任何&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1-2 條 CSS、跨瀏覽器（用 layers / important）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>影響 UX 的視覺改善&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3+ 條 CSS、跨瀏覽器 + framework&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>必要的核心客製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 framework 渲染週期、需要 observer 補打&lt;/td>
 &lt;td>最高&lt;/td>
 &lt;td>改善價值極高才值得&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「成本 vs 改善價值」的對應 — 高成本只用在高價值場景。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>覆寫成本要在開工前報、不能默默承擔。</strong> 客製要對抗多層（UA 預設 + 跨瀏覽器 + framework specificity）時、先把「需要寫多少 / 哪幾條 / 是否有殘留風險」攤開、讓使用者用「視覺改善 vs 維護成本」的角度判斷值不值。</p>
<hr>
<h2 id="為什麼成本要事先告知">為什麼成本要事先告知</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>執行者單方決定「這客製值不值得做」會出現兩種失敗：</p>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>表現</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>執行者覺得值得、使用者覺得太貴</td>
          <td>寫了 5 條 CSS 後使用者說「先還原」、白做</td>
      </tr>
      <tr>
          <td>執行者覺得不值、使用者覺得很重要</td>
          <td>沒做、使用者覺得敷衍</td>
      </tr>
  </tbody>
</table>
<p>兩種失敗都來自「成本與價值的判斷沒對齊」。把成本攤開讓使用者參與判斷、避免兩種失敗。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>要移除 Pagefind filter 的 <code>&lt;details&gt;&lt;summary&gt;</code> disclosure 三角圖示。</p>
<p>我嘗試了：</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">/* 1. UA 預設 marker（Chrome / Firefox） */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">summary</span><span class="p">::</span><span class="nd">marker</span> <span class="p">{</span> <span class="k">content</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">;</span> <span class="k">color</span><span class="p">:</span> <span class="kc">transparent</span><span class="p">;</span> <span class="k">font-size</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c">/* 2. UA 預設 marker（Safari / 老 Chrome） */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">summary</span><span class="p">::</span><span class="nd">-webkit-details-marker</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c">/* 3. 改 summary display 避免 list-item 行為 */</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">summary</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">block</span><span class="p">;</span> <span class="k">list-style</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c">/* 4. 蓋過 pagefind 的 ::after 自訂 chevron */</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__filter-name</span><span class="p">::</span><span class="nd">after</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span> <span class="cp">!important</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c">/* 5. 處理 reset 邊界外的 fallback */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c">/* ... */</span></span></span></code></pre></div><p>寫完後使用者說：「如果要做到這麼大的複寫原設計、先還原回去、先不要做這個變更」。</p>
<h3 id="判讀">判讀</h3>
<p>我的失敗：開工前沒報「這個客製需要對抗 UA + 跨瀏覽器 + framework 三層、預估 5+ 條 CSS、跨瀏覽器有殘留風險」。直接做、使用者看到改動規模才退回。</p>
<p>正確流程應該在嘗試第 2 條 CSS 時就停下來報告：</p>
<blockquote>
<p>移除 disclosure 三角需要對抗：</p>
<ul>
<li>UA 預設 marker（Chrome / Firefox 的 <code>::marker</code>、Safari / 老 Chrome 的 <code>::-webkit-details-marker</code>）</li>
<li>Summary 預設 display: list-item 行為</li>
<li>Pagefind 的自訂 <code>::after</code> chevron</li>
</ul>
<p>預估 4-5 條 CSS 跨 3 種瀏覽器寫法、可能有殘留風險（<code>::marker</code> 在某些瀏覽器版本不能用 display: none）。</p>
<p>視覺改善：filter 名稱旁邊少一個小三角。</p>
<p>值得做嗎？</p></blockquote>
<p>讓使用者判斷再決定。</p>
<h3 id="執行覆寫成本告知-protocol">執行：覆寫成本告知 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>框架 CSS 用 hash class 提升 specificity 嗎？需要 layers / important / 雙寫嗎？</td>
      </tr>
      <tr>
          <td>Framework 渲染週期</td>
          <td>改了會被 reset 嗎？需要 observer 補打嗎？</td>
      </tr>
  </tbody>
</table>
<p>任一層有「需要對抗」就把成本報出來、讓使用者參與決定。</p>
<hr>
<h2 id="內在屬性比較四種覆寫深度">內在屬性比較：四種覆寫深度</h2>
<table>
  <thead>
      <tr>
          <th>深度</th>
          <th>成本</th>
          <th>適合的視覺改善</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 條 CSS、單一瀏覽器</td>
          <td>低</td>
          <td>任何</td>
      </tr>
      <tr>
          <td>1-2 條 CSS、跨瀏覽器（用 layers / important）</td>
          <td>中</td>
          <td>影響 UX 的視覺改善</td>
      </tr>
      <tr>
          <td>3+ 條 CSS、跨瀏覽器 + framework</td>
          <td>高</td>
          <td>必要的核心客製</td>
      </tr>
      <tr>
          <td>跨 framework 渲染週期、需要 observer 補打</td>
          <td>最高</td>
          <td>改善價值極高才值得</td>
      </tr>
  </tbody>
</table>
<p>「成本 vs 改善價值」的對應 — 高成本只用在高價值場景。</p>
<hr>
<h2 id="告知時機與格式">告知時機與格式</h2>
<h3 id="觸發點">觸發點</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該停下來告知</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到第 2 條 CSS 還沒蓋過原設計</td>
          <td>是 — 這通常表示有第二、第三層問題</td>
      </tr>
      <tr>
          <td>跨瀏覽器寫法（<code>-webkit-</code>、<code>::marker</code> 雙寫）</td>
          <td>是 — 跨層成本累積</td>
      </tr>
      <tr>
          <td>加 <code>!important</code></td>
          <td>是 — specificity 戰開始</td>
      </tr>
      <tr>
          <td>加 observer 補打防 reset</td>
          <td>是 — 跟 framework 渲染週期競爭</td>
      </tr>
  </tbody>
</table>
<h3 id="報告格式">報告格式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">這個客製需要：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  - 對抗 [UA / framework / 跨瀏覽器]：N 條 CSS
</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">  - 改善價值：[視覺差異、UX 改善]
</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></code></pre></div><p>讓使用者用「成本 vs 價值」做決定。</p>
<hr>
<h2 id="設計取捨客製深度告知的時機">設計取捨：客製深度告知的時機</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（開工前評估 + 第 2 條 CSS 失敗即告知）當預設、其他做法在特定情境合理。</p>
<h3 id="a開工前評估三層成本開工後第-2-條失敗即告知這個專案的預設">A：開工前評估三層成本、開工後第 2 條失敗即告知（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：開工前評估 UA / framework / 渲染週期三層；開工後寫到第 2 條 CSS 還沒蓋過就停下告知</li>
<li><strong>選 A 的理由</strong>：使用者參與「值不值」判斷、避免做到一半被退回</li>
<li><strong>適合</strong>：所有有覆寫嫌疑的客製</li>
<li><strong>代價</strong>：對話成本（一段成本評估 + 等決定）</li>
</ul>
<h3 id="b寫到第-n-條才告知n--2">B：寫到第 N 條才告知（N &gt; 2）</h3>
<ul>
<li><strong>機制</strong>：執行者自己加碼、到 4-5 條才停</li>
<li><strong>跟 A 的取捨</strong>：B 給執行者更多嘗試空間、A 早提早決；B 在 N 增加時白費時間放大</li>
<li><strong>B 是反模式</strong>：第 2 次失敗就是 <a href="../two-occurrence-threshold/">#42 2 次門檻</a> 的訊號、繼續加碼只是放大沉沒成本</li>
</ul>
<h3 id="c默默寫到底不告知">C：默默寫到底、不告知</h3>
<ul>
<li><strong>機制</strong>：執行者自己承擔、不報告成本</li>
<li><strong>成本特別高的原因</strong>：使用者看到改動規模才退回、所有時間白費</li>
<li><strong>C 才合理的情境</strong>：客製極簡（&lt; 1 條 CSS）、確定不會對抗任何層</li>
</ul>
<h3 id="d開工前直接接受原設計不嘗試">D：開工前直接接受原設計、不嘗試</h3>
<ul>
<li><strong>機制</strong>：看到客製需求是「對抗多層」就直接拒絕、不寫</li>
<li><strong>跟 A 的取捨</strong>：D 完全省成本、A 留空間給「值得做」的客製</li>
<li><strong>D 比 A 好的情境</strong>：使用者明確說「這是視覺微調」+ 對抗三層、且預估成本高 — 直接接受原設計</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該觸發告知的時機</th>
          <th>報告內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到第 2 條 CSS 還沒蓋過原設計</td>
          <td>立刻</td>
          <td>列出對抗的層、預估還要寫多少</td>
      </tr>
      <tr>
          <td>加跨瀏覽器寫法（pseudo 雙寫）</td>
          <td>立刻</td>
          <td>UA 差異、各瀏覽器寫法</td>
      </tr>
      <tr>
          <td>加 <code>!important</code></td>
          <td>立刻</td>
          <td>Specificity 戰開始、考慮 layers</td>
      </tr>
      <tr>
          <td>加 observer 補打</td>
          <td>立刻</td>
          <td>跟 framework 渲染競爭、可能不穩</td>
      </tr>
      <tr>
          <td>改善價值不明顯（純視覺微調）</td>
          <td>開工前</td>
          <td>列改善內容、讓使用者衡量</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：客製成本是使用者該知道的資訊、不是執行者單方承擔。事先告知 = 共同決定 = 不會做到一半被退回。</p>
<p>成本告知有兩種時機 — 本卡聚焦「實作前告知工程成本」、runtime 持續顯示掃描成本見 <a href="../pattern-honest-progress-ui/">#62 Pattern：誠實進度 UX</a>。共同精神是「不 silent 累積負擔」。</p>
]]></content:encoded></item><item><title>同方向反覆失敗的轉折點</title><link>https://tarrragon.github.io/blog/report/failure-direction-pivot-point/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/failure-direction-pivot-point/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>第 2 次同方向失敗、停下來回報「假設可能錯了、要不要換思路」。&lt;/strong> 失敗 ≥ 2 次大多是底層假設有問題、不是執行細節有問題。繼續沿同一方向加碼（換更複雜的 selector、加 &lt;code>!important&lt;/code>、再寫一層 polyfill）只會放大原本的問題。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼第-2-次是轉折點">為什麼第 2 次是轉折點&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>第 1 次失敗常是執行細節（typo、特定 syntax、瀏覽器 cache）— 修正後可能就過。&lt;/p>
&lt;p>第 2 次失敗、用同樣的方法但更小心、還是失敗 — 這個訊號的重量遠大於兩次失敗的相加。它說的是「我以為的問題不在這層、根本問題在別處」。&lt;/p>
&lt;p>第 3 次以上的失敗、加上「再試一次更小心」的努力、產生的副作用會超過解決的問題：&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>1&lt;/td>
 &lt;td>信心足&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>加碼（更複雜的 selector / important）&lt;/td>
 &lt;td>可控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>焦慮&lt;/td>
 &lt;td>全面反擊（layers + important + polyfill）&lt;/td>
 &lt;td>大 — 改動範圍擴張&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4+&lt;/td>
 &lt;td>沉沒成本綁住&lt;/td>
 &lt;td>不肯放棄已寫的&lt;/td>
 &lt;td>嚴重 — 為前面的錯買單&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第 2 次是還能優雅切換方向的最後機會。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把 search scope UI 放在「搜尋框與結果之間」。我嘗試了：&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>Display: contents 串接 + grid-row 排序&lt;/td>
 &lt;td>失敗 — scope 跑到頁尾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>加 &lt;code>!important&lt;/code> 強化 grid-row&lt;/td>
 &lt;td>失敗 — 沒改善&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>Specificity 雙寫（&lt;code>.x.x&lt;/code>）&lt;/td>
 &lt;td>失敗 — 沒改善&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>加更多 display: contents 層&lt;/td>
 &lt;td>失敗 — 同樣結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5（被使用者制止）&lt;/td>
 &lt;td>「思路錯了、換方向」&lt;/td>
 &lt;td>改用 absolute 定位、一次成功&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四次失敗都基於同一假設：「drawer 是 &lt;code>.pagefind-ui&lt;/code> 的直接子節點」。實際 drawer 在 form 內。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>第 2 次失敗時就應該停下來檢查假設、不該再往同方向加碼。&lt;/p>
&lt;p>正確流程：第 1 次失敗修細節；第 2 次失敗用 playwright 量 DOM 確認假設；發現假設錯就立刻換方向、不要為前面的努力買單。&lt;/p>
&lt;h3 id="執行失敗計數與行動">執行：失敗計數與行動&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗次數&lt;/th>
 &lt;th>行動&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>第 1 次&lt;/td>
 &lt;td>修細節（typo、cache、syntax）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 2 次&lt;/td>
 &lt;td>&lt;strong>停下來&lt;/strong> — 用工具驗證底層假設（DOM tree、computed style、framework 行為）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 2 次驗證後&lt;/td>
 &lt;td>假設對 → 繼續修；假設錯 → 換方向、不為前面買單&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵是第 2 次的「停」 — 把行動從「執行更努力」切換到「驗證假設」。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>修細節再試&lt;/td>
 &lt;td>1 次&lt;/td>
 &lt;td>低 — 假設沒問題的話通常成功&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>停下來驗證假設&lt;/td>
 &lt;td>2 次&lt;/td>
 &lt;td>低 — 確認方向是否正確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>加碼（important / 雙寫 / polyfill）&lt;/td>
 &lt;td>不適用&lt;/td>
 &lt;td>高 — 假設錯時放大問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>換方向（重新設計實作）&lt;/td>
 &lt;td>2 次後驗證假設錯&lt;/td>
 &lt;td>中 — 一次性成本、後續穩定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇規則：&lt;strong>第 1 次修細節、第 2 次驗證、第 2 次後驗證假設決定繼續或換方向&lt;/strong>。不該有第 3 次同方向加碼。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>第 2 次同方向失敗、停下來回報「假設可能錯了、要不要換思路」。</strong> 失敗 ≥ 2 次大多是底層假設有問題、不是執行細節有問題。繼續沿同一方向加碼（換更複雜的 selector、加 <code>!important</code>、再寫一層 polyfill）只會放大原本的問題。</p>
<hr>
<h2 id="為什麼第-2-次是轉折點">為什麼第 2 次是轉折點</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>第 1 次失敗常是執行細節（typo、特定 syntax、瀏覽器 cache）— 修正後可能就過。</p>
<p>第 2 次失敗、用同樣的方法但更小心、還是失敗 — 這個訊號的重量遠大於兩次失敗的相加。它說的是「我以為的問題不在這層、根本問題在別處」。</p>
<p>第 3 次以上的失敗、加上「再試一次更小心」的努力、產生的副作用會超過解決的問題：</p>
<table>
  <thead>
      <tr>
          <th>嘗試次數</th>
          <th>心理狀態</th>
          <th>行動模式</th>
          <th>可能副作用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>信心足</td>
          <td>直接做</td>
          <td>無</td>
      </tr>
      <tr>
          <td>2</td>
          <td>信心動搖</td>
          <td>加碼（更複雜的 selector / important）</td>
          <td>可控</td>
      </tr>
      <tr>
          <td>3</td>
          <td>焦慮</td>
          <td>全面反擊（layers + important + polyfill）</td>
          <td>大 — 改動範圍擴張</td>
      </tr>
      <tr>
          <td>4+</td>
          <td>沉沒成本綁住</td>
          <td>不肯放棄已寫的</td>
          <td>嚴重 — 為前面的錯買單</td>
      </tr>
  </tbody>
</table>
<p>第 2 次是還能優雅切換方向的最後機會。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>要把 search scope UI 放在「搜尋框與結果之間」。我嘗試了：</p>
<table>
  <thead>
      <tr>
          <th>嘗試</th>
          <th>方向</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Display: contents 串接 + grid-row 排序</td>
          <td>失敗 — scope 跑到頁尾</td>
      </tr>
      <tr>
          <td>2</td>
          <td>加 <code>!important</code> 強化 grid-row</td>
          <td>失敗 — 沒改善</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Specificity 雙寫（<code>.x.x</code>）</td>
          <td>失敗 — 沒改善</td>
      </tr>
      <tr>
          <td>4</td>
          <td>加更多 display: contents 層</td>
          <td>失敗 — 同樣結果</td>
      </tr>
      <tr>
          <td>5（被使用者制止）</td>
          <td>「思路錯了、換方向」</td>
          <td>改用 absolute 定位、一次成功</td>
      </tr>
  </tbody>
</table>
<p>四次失敗都基於同一假設：「drawer 是 <code>.pagefind-ui</code> 的直接子節點」。實際 drawer 在 form 內。</p>
<h3 id="判讀">判讀</h3>
<p>第 2 次失敗時就應該停下來檢查假設、不該再往同方向加碼。</p>
<p>正確流程：第 1 次失敗修細節；第 2 次失敗用 playwright 量 DOM 確認假設；發現假設錯就立刻換方向、不要為前面的努力買單。</p>
<h3 id="執行失敗計數與行動">執行：失敗計數與行動</h3>
<table>
  <thead>
      <tr>
          <th>失敗次數</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 次</td>
          <td>修細節（typo、cache、syntax）</td>
      </tr>
      <tr>
          <td>第 2 次</td>
          <td><strong>停下來</strong> — 用工具驗證底層假設（DOM tree、computed style、framework 行為）</td>
      </tr>
      <tr>
          <td>第 2 次驗證後</td>
          <td>假設對 → 繼續修；假設錯 → 換方向、不為前面買單</td>
      </tr>
  </tbody>
</table>
<p>關鍵是第 2 次的「停」 — 把行動從「執行更努力」切換到「驗證假設」。</p>
<hr>
<h2 id="內在屬性比較四種失敗應對">內在屬性比較：四種失敗應對</h2>
<table>
  <thead>
      <tr>
          <th>應對</th>
          <th>適用次數</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修細節再試</td>
          <td>1 次</td>
          <td>低 — 假設沒問題的話通常成功</td>
      </tr>
      <tr>
          <td>停下來驗證假設</td>
          <td>2 次</td>
          <td>低 — 確認方向是否正確</td>
      </tr>
      <tr>
          <td>加碼（important / 雙寫 / polyfill）</td>
          <td>不適用</td>
          <td>高 — 假設錯時放大問題</td>
      </tr>
      <tr>
          <td>換方向（重新設計實作）</td>
          <td>2 次後驗證假設錯</td>
          <td>中 — 一次性成本、後續穩定</td>
      </tr>
  </tbody>
</table>
<p>選擇規則：<strong>第 1 次修細節、第 2 次驗證、第 2 次後驗證假設決定繼續或換方向</strong>。不該有第 3 次同方向加碼。</p>
<hr>
<h2 id="假設驗證的具體方法">假設驗證的具體方法</h2>
<h3 id="1-用工具讀真實狀態">1. 用工具讀真實狀態</h3>
<table>
  <thead>
      <tr>
          <th>假設類型</th>
          <th>驗證工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM 結構</td>
          <td>playwright <code>browser_evaluate</code> 讀 ancestor chain</td>
      </tr>
      <tr>
          <td>Computed style</td>
          <td>playwright <code>getComputedStyle</code></td>
      </tr>
      <tr>
          <td>元素位置</td>
          <td>playwright <code>getBoundingClientRect</code></td>
      </tr>
      <tr>
          <td>Framework 行為</td>
          <td>讀框架 source、看 reconcile 條件</td>
      </tr>
  </tbody>
</table>
<h3 id="2-反問如果假設錯了會怎樣">2. 反問「如果假設錯了會怎樣」</h3>
<table>
  <thead>
      <tr>
          <th>假設</th>
          <th>如果錯了</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Drawer 是 form 的 sibling</td>
          <td>那 grid-row 完全無效（drawer 跟 form 共用 cell）</td>
      </tr>
      <tr>
          <td>Specificity 30 是上限</td>
          <td>那 layers 才是解、不是雙寫</td>
      </tr>
  </tbody>
</table>
<p>「如果錯了會怎樣」的答案是「跟我看到的失敗一致」 → 假設可能錯。</p>
<h3 id="3-對外回報">3. 對外回報</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">我嘗試了兩次 [方向 X]、結果都 [現象 Y]。
</span></span><span class="line"><span class="ln">2</span><span class="cl">我的假設是 [假設 Z]、但驗證 [假設 Z] 似乎不成立。
</span></span><span class="line"><span class="ln">3</span><span class="cl">要不要換 [方向 W]、或是有什麼資訊我沒看到？</span></span></code></pre></div><p>對外回報 = 把問題放到使用者視野、避免繼續單方面加碼。</p>
<hr>
<h2 id="設計取捨失敗應對的策略">設計取捨：失敗應對的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（第 2 次失敗驗證假設）當預設、其他做法在特定情境合理。</p>
<blockquote>
<p>本篇是 <a href="../two-occurrence-threshold/">#42 2 次門檻</a> 抽象原則在「同方向失敗」這個面向的應用。</p></blockquote>
<h3 id="a第-2-次失敗停下驗證假設這個專案的預設">A：第 2 次失敗停下驗證假設（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：第 1 次修細節再試；第 2 次失敗 → 用工具驗證底層假設（DOM tree、computed style、framework 行為）；驗證錯就換方向</li>
<li><strong>選 A 的理由</strong>：早一點切換、雙方時間都省；2 次失敗的證據量足以判斷「路徑問題」</li>
<li><strong>適合</strong>：所有除錯情境</li>
<li><strong>代價</strong>：第 2 次後的「停下」需要心理紀律（克服繼續加碼的衝動）</li>
</ul>
<h3 id="b第-4-5-次才停沉沒成本綁住">B：第 4-5 次才停（沉沒成本綁住）</h3>
<ul>
<li><strong>機制</strong>：繼續加碼直到使用者制止</li>
<li><strong>跟 A 的取捨</strong>：B 給更多嘗試空間、A 早決；B 在沉沒成本累積後更難切換</li>
<li><strong>B 是反模式</strong>：沉沒成本是認知偏誤、不是合理應對 — 「再試一次更小心」的衝動是訊號、不是解法</li>
</ul>
<h3 id="c第-1-次失敗就換方向過度反應">C：第 1 次失敗就換方向（過度反應）</h3>
<ul>
<li><strong>機制</strong>：每次失敗都假設方向錯、立即換</li>
<li><strong>跟 A 的取捨</strong>：C 太敏感、A 適度；C 在「修細節就能過」的場景過度切換</li>
<li><strong>C 才合理的情境</strong>：嘗試成本極高（每次失敗 = 半天工作）— 即使單次失敗、也值得停下重新評估</li>
</ul>
<h3 id="d永不換方向">D：永不換方向</h3>
<ul>
<li><strong>機制</strong>：認定方向對、無限加碼</li>
<li><strong>D 是反模式</strong>：方向錯時無法收斂、最後產生脆弱的 patchwork</li>
<li><strong>看起來吸引人的原因</strong>：心理上不想承認方向錯、繼續加碼比放棄好受</li>
<li><strong>實際發生的代價</strong>：失敗訊號被忽略、產生脆弱的 patchwork、修復成本指數放大</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該觸發的行動</th>
          <th>第一個該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 2 次同方向失敗</td>
          <td>停下來驗證假設</td>
          <td>用 playwright / DevTools 量真實狀態</td>
      </tr>
      <tr>
          <td>加 <code>!important</code> 解 specificity</td>
          <td>停 — 切換到 layers 思路</td>
          <td>評估用 CSS Layers</td>
      </tr>
      <tr>
          <td>加第 2 條 polyfill 補跨瀏覽器</td>
          <td>停 — 評估值不值得繼續</td>
          <td>報告成本、問使用者意願</td>
      </tr>
      <tr>
          <td>用 imperative JS 補宣告式 layout</td>
          <td>停 — 切換到 CSS-first 思路</td>
          <td>評估能否用 grid / flex 解決</td>
      </tr>
      <tr>
          <td>內心 OS：「再試一次更小心」</td>
          <td>停 — 這是沉沒成本綁住的訊號</td>
          <td>對外回報、邀請換方向</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：第 2 次失敗的最佳行動是「驗證假設」、不是「再試一次」。早一點切換方向、節省的是雙方時間。</p>
<p>「再試一次」是當下便利的選項（不需要重新分析）、「驗證假設換方向」是對齊正確性的選項 — 這個反相關見 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。</p>
]]></content:encoded></item><item><title>「可決定」與「該先確認」的邊界</title><link>https://tarrragon.github.io/blog/report/decide-vs-confirm-boundary/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/decide-vs-confirm-boundary/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>「可決定」與「該先確認」的分界線是「使用者會看到嗎」。&lt;/strong> 純技術實作細節（用 grid 或 flex、用 ResizeObserver 或 setInterval）可以自決；但只要結果會在 UI 上出現（數字、順序、文字、配色），就要先給選項讓使用者點頭。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼要分這條線">為什麼要分這條線&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>執行者自決所有事 = 強迫使用者用「視覺驗收」做決定 — 看到結果才能說「這不對」、不對就要重做。&lt;/p>
&lt;p>把「使用者會看到的決定」拉出來事先確認 = 在實作前讓使用者參與決策、避免實作後被退回。&lt;/p>
&lt;p>但所有事都要確認 = 對話成本爆炸、使用者疲勞、效率低。&lt;/p>
&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>Filter sidebar breakpoint = 1400px&lt;/td>
 &lt;td>該確認 — 使用者視窗大小不確定&lt;/td>
 &lt;td>自決寫死&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter 順序 type → tag&lt;/td>
 &lt;td>該確認 — UX 偏好&lt;/td>
 &lt;td>自決寫死、後被肯定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scope-h 預設值 = 56px&lt;/td>
 &lt;td>該確認 — 視覺差異明顯&lt;/td>
 &lt;td>自決寫死、後被否定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter 寬度 400px&lt;/td>
 &lt;td>使用者已給數值&lt;/td>
 &lt;td>直接用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用 absolute 定位而非 grid&lt;/td>
 &lt;td>純技術選擇&lt;/td>
 &lt;td>自決、無需確認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用 ResizeObserver 而非 setInterval&lt;/td>
 &lt;td>純技術選擇&lt;/td>
 &lt;td>自決、無需確認&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>可以看出規則：使用者會看到的視覺結果（breakpoint 對應的 viewport 行為、順序對應的閱讀體驗、預設值對應的初始視覺）都該確認；底層工具選擇（grid / flex / absolute、ResizeObserver / setInterval）可以自決。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>判斷標準是「這個決定會在 UI 上產生使用者能感知的差異嗎」：&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>Visible 數字&lt;/td>
 &lt;td>breakpoint、預設尺寸、初始值&lt;/td>
 &lt;td>內部 calculation 細節&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>順序&lt;/td>
 &lt;td>filter 排序、選單排序&lt;/td>
 &lt;td>DOM tree 排序（不影響視覺）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文字&lt;/td>
 &lt;td>UI 標籤、訊息&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>grid / flex / observer 等&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="執行自決前的兩問">執行：自決前的兩問&lt;/h3>
&lt;p>每個決定執行前先問：&lt;/p>
&lt;ol>
&lt;li>這個決定在 UI 上會產生使用者能感知的差異嗎？&lt;/li>
&lt;li>是不是有多個合理選項、選不同會影響使用者體驗？&lt;/li>
&lt;/ol>
&lt;p>兩個都「是」 → 該確認。&lt;/p>
&lt;p>兩個都「否」或只有一個「是」 → 可自決。&lt;/p>
&lt;hr>
&lt;h2 id="確認的格式給選項而非開放問">確認的格式：給選項而非開放問&lt;/h2>
&lt;h3 id="較差的問法">較差的問法&lt;/h3>
&lt;blockquote>
&lt;p>「Breakpoint 應該設多少？」&lt;/p>&lt;/blockquote>
&lt;p>開放問題、使用者要自己想出答案。&lt;/p>
&lt;h3 id="較好的問法">較好的問法&lt;/h3>
&lt;blockquote>
&lt;p>「Breakpoint 我預估三個選項：&lt;/p>
&lt;ul>
&lt;li>1280px：物理上能放下、但邊緣情境可能擠壓&lt;/li>
&lt;li>1400px：較安全、有 120px 餘裕&lt;/li>
&lt;li>1564px：完全不溢出 body padding、最保守&lt;/li>
&lt;/ul>
&lt;p>我會選 1400px 作為平衡。OK 嗎？或要其他？」&lt;/p>&lt;/blockquote>
&lt;p>給選項 + 推薦 + 開放修改空間 — 使用者可以「OK」「換 1280」「再放寬到 1600」三種回應。對話成本低。&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>0&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>純技術實作細節&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全部確認&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>不適用、太煩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自決技術細節、確認 visible 決定&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>涉及 UX 偏好的決定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優先「自決技術細節 + 列選項確認 visible 決定」 — 對話成本低、重做風險低。&lt;/p>
&lt;hr>
&lt;h2 id="使用者會看到嗎的測試">「使用者會看到嗎」的測試&lt;/h2>
&lt;p>當不確定某個決定該確認還是自決時、用三個測試：&lt;/p>
&lt;h3 id="1-ui-變動測試">1. UI 變動測試&lt;/h3>
&lt;p>「不同選擇會在 UI 上看到差異嗎？」&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>「可決定」與「該先確認」的分界線是「使用者會看到嗎」。</strong> 純技術實作細節（用 grid 或 flex、用 ResizeObserver 或 setInterval）可以自決；但只要結果會在 UI 上出現（數字、順序、文字、配色），就要先給選項讓使用者點頭。</p>
<hr>
<h2 id="為什麼要分這條線">為什麼要分這條線</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>執行者自決所有事 = 強迫使用者用「視覺驗收」做決定 — 看到結果才能說「這不對」、不對就要重做。</p>
<p>把「使用者會看到的決定」拉出來事先確認 = 在實作前讓使用者參與決策、避免實作後被退回。</p>
<p>但所有事都要確認 = 對話成本爆炸、使用者疲勞、效率低。</p>
<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>Filter sidebar breakpoint = 1400px</td>
          <td>該確認 — 使用者視窗大小不確定</td>
          <td>自決寫死</td>
      </tr>
      <tr>
          <td>Filter 順序 type → tag</td>
          <td>該確認 — UX 偏好</td>
          <td>自決寫死、後被肯定</td>
      </tr>
      <tr>
          <td>Scope-h 預設值 = 56px</td>
          <td>該確認 — 視覺差異明顯</td>
          <td>自決寫死、後被否定</td>
      </tr>
      <tr>
          <td>Filter 寬度 400px</td>
          <td>使用者已給數值</td>
          <td>直接用</td>
      </tr>
      <tr>
          <td>用 absolute 定位而非 grid</td>
          <td>純技術選擇</td>
          <td>自決、無需確認</td>
      </tr>
      <tr>
          <td>用 ResizeObserver 而非 setInterval</td>
          <td>純技術選擇</td>
          <td>自決、無需確認</td>
      </tr>
  </tbody>
</table>
<p>可以看出規則：使用者會看到的視覺結果（breakpoint 對應的 viewport 行為、順序對應的閱讀體驗、預設值對應的初始視覺）都該確認；底層工具選擇（grid / flex / absolute、ResizeObserver / setInterval）可以自決。</p>
<h3 id="判讀">判讀</h3>
<p>判斷標準是「這個決定會在 UI 上產生使用者能感知的差異嗎」：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>該先確認</th>
          <th>可自決</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Visible 數字</td>
          <td>breakpoint、預設尺寸、初始值</td>
          <td>內部 calculation 細節</td>
      </tr>
      <tr>
          <td>順序</td>
          <td>filter 排序、選單排序</td>
          <td>DOM tree 排序（不影響視覺）</td>
      </tr>
      <tr>
          <td>文字</td>
          <td>UI 標籤、訊息</td>
          <td>內部變數名</td>
      </tr>
      <tr>
          <td>配色 / 視覺樣式</td>
          <td>使用者看得到</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>技術選擇</td>
          <td>不適用</td>
          <td>grid / flex / observer 等</td>
      </tr>
  </tbody>
</table>
<h3 id="執行自決前的兩問">執行：自決前的兩問</h3>
<p>每個決定執行前先問：</p>
<ol>
<li>這個決定在 UI 上會產生使用者能感知的差異嗎？</li>
<li>是不是有多個合理選項、選不同會影響使用者體驗？</li>
</ol>
<p>兩個都「是」 → 該確認。</p>
<p>兩個都「否」或只有一個「是」 → 可自決。</p>
<hr>
<h2 id="確認的格式給選項而非開放問">確認的格式：給選項而非開放問</h2>
<h3 id="較差的問法">較差的問法</h3>
<blockquote>
<p>「Breakpoint 應該設多少？」</p></blockquote>
<p>開放問題、使用者要自己想出答案。</p>
<h3 id="較好的問法">較好的問法</h3>
<blockquote>
<p>「Breakpoint 我預估三個選項：</p>
<ul>
<li>1280px：物理上能放下、但邊緣情境可能擠壓</li>
<li>1400px：較安全、有 120px 餘裕</li>
<li>1564px：完全不溢出 body padding、最保守</li>
</ul>
<p>我會選 1400px 作為平衡。OK 嗎？或要其他？」</p></blockquote>
<p>給選項 + 推薦 + 開放修改空間 — 使用者可以「OK」「換 1280」「再放寬到 1600」三種回應。對話成本低。</p>
<hr>
<h2 id="內在屬性比較四種決策模式">內在屬性比較：四種決策模式</h2>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>對話成本</th>
          <th>重做風險</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全部自決</td>
          <td>0</td>
          <td>高</td>
          <td>純技術實作細節</td>
      </tr>
      <tr>
          <td>全部確認</td>
          <td>高</td>
          <td>0</td>
          <td>不適用、太煩</td>
      </tr>
      <tr>
          <td>自決技術細節、確認 visible 決定</td>
          <td>低</td>
          <td>低</td>
          <td>一般情境</td>
      </tr>
      <tr>
          <td>列選項 + 推薦讓使用者選</td>
          <td>低</td>
          <td>最低</td>
          <td>涉及 UX 偏好的決定</td>
      </tr>
  </tbody>
</table>
<p>優先「自決技術細節 + 列選項確認 visible 決定」 — 對話成本低、重做風險低。</p>
<hr>
<h2 id="使用者會看到嗎的測試">「使用者會看到嗎」的測試</h2>
<p>當不確定某個決定該確認還是自決時、用三個測試：</p>
<h3 id="1-ui-變動測試">1. UI 變動測試</h3>
<p>「不同選擇會在 UI 上看到差異嗎？」</p>
<table>
  <thead>
      <tr>
          <th>例</th>
          <th>答</th>
          <th>結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用 grid 還是 flex 排兩欄</td>
          <td>否（最終視覺一樣）</td>
          <td>自決</td>
      </tr>
      <tr>
          <td>Breakpoint 1280 vs 1400</td>
          <td>是（不同 viewport 下行為不同）</td>
          <td>確認</td>
      </tr>
  </tbody>
</table>
<h3 id="2-ux-偏好測試">2. UX 偏好測試</h3>
<p>「選不同會讓使用者覺得體驗不同嗎？」</p>
<table>
  <thead>
      <tr>
          <th>例</th>
          <th>答</th>
          <th>結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter 順序 type 在前 vs tag 在前</td>
          <td>是（掃描成本不同）</td>
          <td>確認</td>
      </tr>
      <tr>
          <td>用 ResizeObserver 還是 setInterval</td>
          <td>否（使用者感知不到差異）</td>
          <td>自決</td>
      </tr>
  </tbody>
</table>
<h3 id="3-不可逆測試">3. 不可逆測試</h3>
<p>「這個決定寫進 commit 後、之後改成本高嗎？」</p>
<table>
  <thead>
      <tr>
          <th>例</th>
          <th>答</th>
          <th>結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Section 名稱（content/report/）</td>
          <td>是（改動要 redirect、broken link）</td>
          <td>確認</td>
      </tr>
      <tr>
          <td>內部 helper function 名稱</td>
          <td>否（rename 一行 grep）</td>
          <td>自決</td>
      </tr>
  </tbody>
</table>
<p>三個測試任一個「是」 → 確認。</p>
<hr>
<h2 id="設計取捨決定權分配的策略">設計取捨：決定權分配的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（visible 決定先確認 + 技術自決 + 給選項）當預設、其他做法在特定情境合理。</p>
<h3 id="avisible-決定先確認技術自決確認時給選項這個專案的預設">A：Visible 決定先確認、技術自決、確認時給選項（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：使用者會看到的數字 / 順序 / 文字先列選項給使用者選；純技術實作（grid / flex / observer）自決</li>
<li><strong>選 A 的理由</strong>：對話成本低（只確認該確認的）+ 重做風險低（visible 決定使用者參與）</li>
<li><strong>適合</strong>：所有客製情境</li>
<li><strong>代價</strong>：執行者要主動分辨「visible vs 技術細節」、給選項時要先想出選項</li>
</ul>
<h3 id="b全部自決">B：全部自決</h3>
<ul>
<li><strong>機制</strong>：執行者自己決定所有事</li>
<li><strong>跟 A 的取捨</strong>：B 對話成本 0、A 適度；B 容易做到使用者不要的東西</li>
<li><strong>B 才合理的情境</strong>：純內部工具、執行者就是使用者；或時間極緊、優先求動</li>
</ul>
<h3 id="c全部確認">C：全部確認</h3>
<ul>
<li><strong>機制</strong>：每個決定都先問</li>
<li><strong>跟 A 的取捨</strong>：C 重做風險 0、A 適度；C 對話成本爆炸、使用者疲勞</li>
<li><strong>C 才合理的情境</strong>：完全不熟悉的領域、執行者沒有判斷能力（罕見、應透過學習脫離此狀態）</li>
</ul>
<h3 id="d開放問題不給選項">D：開放問題、不給選項</h3>
<ul>
<li><strong>機制</strong>：直接問「breakpoint 應該設多少」</li>
<li><strong>跟 A 的取捨</strong>：D 把分析負擔丟給使用者、A 執行者先做分析給選項</li>
<li><strong>D 才合理的情境</strong>：使用者已經有明確答案、只需要傳達 — 否則 A 的「給選項 + 推薦」更有效率</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即將寫死一個 visible 數字（padding、margin、breakpoint）</td>
          <td>列選項確認、不要自決</td>
      </tr>
      <tr>
          <td>即將決定一個 visible 順序（filter、選單）</td>
          <td>列選項確認</td>
      </tr>
      <tr>
          <td>即將寫使用者會看到的文字（label、訊息）</td>
          <td>確認措辭</td>
      </tr>
      <tr>
          <td>即將選擇技術實作（grid / flex / observer）</td>
          <td>自決</td>
      </tr>
      <tr>
          <td>即將命名內部 helper / 變數</td>
          <td>自決</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：能影響使用者體驗的決定屬於使用者、能影響執行者效率的決定屬於執行者。分清楚就不會把該確認的事自決、也不會把該自決的事拿去煩使用者。</p>
<p>第三輪指令類型現有五類：空間（#16）/ 相對位置（#17）/ 隔離（#18）/ 決定權（本卡 #21）/ 篩選（<a href="../filter-instruction-clarification/">#58</a>）。前四類缺幾何 / 邊界 / 拍板資訊、第 5 類缺操作層級資訊。</p>
]]></content:encoded></item><item><title>「先還原」「先重來」類退出指令的處理</title><link>https://tarrragon.github.io/blog/report/revert-instruction-handling/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/revert-instruction-handling/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>退出指令（「先還原」「先重來」「先放著」）的執行前要先確認兩件事：還原到哪個狀態、要不要先存 checkpoint。&lt;/strong> 直接刪掉當前進度、未來想比較沒得比、想恢復也找不到。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼退出指令需要-protocol">為什麼退出指令需要 protocol&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「先還原」&lt;/td>
 &lt;td>暫時不用、之後可能重做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先還原」&lt;/td>
 &lt;td>完全放棄、不會再做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先還原」&lt;/td>
 &lt;td>換個方向重做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先重來」&lt;/td>
 &lt;td>從上一個 commit 重新試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先重來」&lt;/td>
 &lt;td>從更早的點重新試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>執行者不確認意圖、直接刪 = 把使用者的選項收窄成「沒有了」。事先確認 = 為使用者保留可能性。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>使用者說：「如果要做到這麼大的複寫原設計、我們先還原回去、先不要做這個變更」。&lt;/p>
&lt;p>我立刻刪掉那段 CSS、繼續往下做其他事。沒問「還原到哪」、沒先 commit checkpoint。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>後來想比較「複雜覆寫版本」與「接受原設計版本」的差異 — 沒得比。「複雜覆寫版本」已經不存在於 git 歷史。&lt;/p>
&lt;p>正確流程應該是：&lt;/p>
&lt;ol>
&lt;li>收到「還原」指令時、先回應「我會先 commit 當前進度（即使未採用）作為 checkpoint，再還原到 X 狀態。OK 嗎？」&lt;/li>
&lt;li>確認後、commit 當前狀態（不 push）、commit message 標明「探索版本、未採用」&lt;/li>
&lt;li>還原到 X 狀態&lt;/li>
&lt;li>繼續做後續事&lt;/li>
&lt;/ol>
&lt;p>未來想比較或恢復、checkpoint 都還在。&lt;/p>
&lt;h3 id="執行退出指令的-protocol">執行：退出指令的 protocol&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>1&lt;/td>
 &lt;td>暫停執行、不要立刻刪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>確認「還原到哪個狀態」（上個 commit / 特定 commit / 完全乾淨）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>評估「當前進度有保留價值嗎」 — 探索成果、未採用方案、可能未來重用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>有保留價值 → 先 commit checkpoint、message 標明「探索、未採用」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>執行還原（reset / checkout / revert）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6&lt;/td>
 &lt;td>確認還原後狀態符合使用者意圖&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="checkpoint-的命名與-commit-message">Checkpoint 的命名與 commit message&lt;/h2>
&lt;p>當前進度作為 checkpoint commit、message 應該包含：&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">chore(explore): N+1 attempts to remove disclosure marker
&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">探索方向：用 ::-webkit-details-marker / ::marker / display: block 三層
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">覆寫移除 disclosure 三角圖示。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">寫了 5 條 CSS 跨 3 種瀏覽器、覆寫成本過高。
&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;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">此 commit 保留探索成果、未來若評估值得做時可參考。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵元素：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>chore(explore):&lt;/code>&lt;/strong> prefix — 標明非正式採用&lt;/li>
&lt;li>&lt;strong>探索方向&lt;/strong> — 說明做了什麼&lt;/li>
&lt;li>&lt;strong>最終決定&lt;/strong> — 為什麼不採用&lt;/li>
&lt;li>&lt;strong>保留理由&lt;/strong> — 未來何時可能重用&lt;/li>
&lt;/ul>
&lt;p>未來人看到這個 commit、知道「這是探索、不是當前實作」、不會誤用。&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;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>git reset --hard&lt;/code>（未 commit 直接丟）&lt;/td>
 &lt;td>低 — git reflog 短期還在&lt;/td>
 &lt;td>困難&lt;/td>
 &lt;td>確定不要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>git stash&lt;/code>（暫存到 stash）&lt;/td>
 &lt;td>中 — stash 可隨時恢復&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>短期暫停&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>git commit&lt;/code> checkpoint 後 reset / revert&lt;/td>
 &lt;td>高 — commit 永久存在&lt;/td>
 &lt;td>容易&lt;/td>
 &lt;td>探索性嘗試、未採用方案&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優先選 checkpoint 方式 — 為未來保留比較與恢復的可能性。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「先還原」&lt;/td>
 &lt;td>暫時、可能重做&lt;/td>
 &lt;td>Checkpoint + 還原&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先還原、不要做了」&lt;/td>
 &lt;td>確定放棄&lt;/td>
 &lt;td>Checkpoint（小機率重看） + 還原&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「重來」&lt;/td>
 &lt;td>換個方向&lt;/td>
 &lt;td>Checkpoint + 還原到起點&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;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>退出指令（「先還原」「先重來」「先放著」）的執行前要先確認兩件事：還原到哪個狀態、要不要先存 checkpoint。</strong> 直接刪掉當前進度、未來想比較沒得比、想恢復也找不到。</p>
<hr>
<h2 id="為什麼退出指令需要-protocol">為什麼退出指令需要 protocol</h2>
<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>從上一個 commit 重新試</td>
      </tr>
      <tr>
          <td>「先重來」</td>
          <td>從更早的點重新試</td>
      </tr>
  </tbody>
</table>
<p>執行者不確認意圖、直接刪 = 把使用者的選項收窄成「沒有了」。事先確認 = 為使用者保留可能性。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>使用者說：「如果要做到這麼大的複寫原設計、我們先還原回去、先不要做這個變更」。</p>
<p>我立刻刪掉那段 CSS、繼續往下做其他事。沒問「還原到哪」、沒先 commit checkpoint。</p>
<h3 id="判讀">判讀</h3>
<p>後來想比較「複雜覆寫版本」與「接受原設計版本」的差異 — 沒得比。「複雜覆寫版本」已經不存在於 git 歷史。</p>
<p>正確流程應該是：</p>
<ol>
<li>收到「還原」指令時、先回應「我會先 commit 當前進度（即使未採用）作為 checkpoint，再還原到 X 狀態。OK 嗎？」</li>
<li>確認後、commit 當前狀態（不 push）、commit message 標明「探索版本、未採用」</li>
<li>還原到 X 狀態</li>
<li>繼續做後續事</li>
</ol>
<p>未來想比較或恢復、checkpoint 都還在。</p>
<h3 id="執行退出指令的-protocol">執行：退出指令的 protocol</h3>
<p>收到退出指令時、依序處理：</p>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>暫停執行、不要立刻刪</td>
      </tr>
      <tr>
          <td>2</td>
          <td>確認「還原到哪個狀態」（上個 commit / 特定 commit / 完全乾淨）</td>
      </tr>
      <tr>
          <td>3</td>
          <td>評估「當前進度有保留價值嗎」 — 探索成果、未採用方案、可能未來重用</td>
      </tr>
      <tr>
          <td>4</td>
          <td>有保留價值 → 先 commit checkpoint、message 標明「探索、未採用」</td>
      </tr>
      <tr>
          <td>5</td>
          <td>執行還原（reset / checkout / revert）</td>
      </tr>
      <tr>
          <td>6</td>
          <td>確認還原後狀態符合使用者意圖</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="checkpoint-的命名與-commit-message">Checkpoint 的命名與 commit message</h2>
<p>當前進度作為 checkpoint commit、message 應該包含：</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">chore(explore): N+1 attempts to remove disclosure marker
</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">探索方向：用 ::-webkit-details-marker / ::marker / display: block 三層
</span></span><span class="line"><span class="ln">4</span><span class="cl">覆寫移除 disclosure 三角圖示。
</span></span><span class="line"><span class="ln">5</span><span class="cl">寫了 5 條 CSS 跨 3 種瀏覽器、覆寫成本過高。
</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><span class="line"><span class="ln">8</span><span class="cl">此 commit 保留探索成果、未來若評估值得做時可參考。</span></span></code></pre></div><p>關鍵元素：</p>
<ul>
<li><strong><code>chore(explore):</code></strong> prefix — 標明非正式採用</li>
<li><strong>探索方向</strong> — 說明做了什麼</li>
<li><strong>最終決定</strong> — 為什麼不採用</li>
<li><strong>保留理由</strong> — 未來何時可能重用</li>
</ul>
<p>未來人看到這個 commit、知道「這是探索、不是當前實作」、不會誤用。</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>不適用</td>
      </tr>
      <tr>
          <td><code>git reset --hard</code>（未 commit 直接丟）</td>
          <td>低 — git reflog 短期還在</td>
          <td>困難</td>
          <td>確定不要</td>
      </tr>
      <tr>
          <td><code>git stash</code>（暫存到 stash）</td>
          <td>中 — stash 可隨時恢復</td>
          <td>中</td>
          <td>短期暫停</td>
      </tr>
      <tr>
          <td><code>git commit</code> checkpoint 後 reset / revert</td>
          <td>高 — commit 永久存在</td>
          <td>容易</td>
          <td>探索性嘗試、未採用方案</td>
      </tr>
  </tbody>
</table>
<p>優先選 checkpoint 方式 — 為未來保留比較與恢復的可能性。</p>
<hr>
<h2 id="退出意圖的辨識">退出意圖的辨識</h2>
<h3 id="從語氣判斷">從語氣判斷</h3>
<table>
  <thead>
      <tr>
          <th>語氣</th>
          <th>對應意圖</th>
          <th>推薦處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「先還原」</td>
          <td>暫時、可能重做</td>
          <td>Checkpoint + 還原</td>
      </tr>
      <tr>
          <td>「先還原、不要做了」</td>
          <td>確定放棄</td>
          <td>Checkpoint（小機率重看） + 還原</td>
      </tr>
      <tr>
          <td>「重來」</td>
          <td>換個方向</td>
          <td>Checkpoint + 還原到起點</td>
      </tr>
      <tr>
          <td>「不要這樣寫」</td>
          <td>局部修正、不是全還原</td>
          <td>局部改、不要全還原</td>
      </tr>
  </tbody>
</table>
<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">你想要：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  □ 完全還原、刪掉這次的 CSS
</span></span><span class="line"><span class="ln">3</span><span class="cl">  □ 還原但保留 commit checkpoint、未來可能參考
</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></code></pre></div><hr>
<h2 id="設計取捨退出指令的處理策略">設計取捨：退出指令的處理策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（Checkpoint + 確認還原範圍）當預設、其他做法在特定情境合理。</p>
<h3 id="acommit-checkpoint--確認還原範圍這個專案的預設">A：Commit checkpoint + 確認還原範圍（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：commit 當前進度（標明「探索、未採用」）+ 問「還原到哪個 commit」+ 執行還原</li>
<li><strong>選 A 的理由</strong>：未來想比較或恢復都還在、checkpoint 成本只是多一個 commit</li>
<li><strong>適合</strong>：探索性嘗試、未採用方案的退出</li>
<li><strong>代價</strong>：多一個 commit 在歷史中（可後續 squash 或保留）</li>
</ul>
<h3 id="bgit-stash-暫存">B：<code>git stash</code> 暫存</h3>
<ul>
<li><strong>機制</strong>：把未 commit 的變更存到 stash、不污染 commit 歷史</li>
<li><strong>跟 A 的取捨</strong>：B 不留 commit、A 留 commit；B 適合「短期暫停、之後恢復」、A 適合「長期探索紀錄」</li>
<li><strong>B 比 A 好的情境</strong>：暫時切到別的工作、之後一定會回來繼續</li>
</ul>
<h3 id="cgit-reset---hard直接丟">C：<code>git reset --hard</code>（直接丟）</h3>
<ul>
<li><strong>機制</strong>：reset 到指定 commit、未 commit 的變更全失去</li>
<li><strong>跟 A 的取捨</strong>：C 完全清乾淨、A 保留紀錄；C 在 git reflog 內短期還能恢復、但之後永久消失</li>
<li><strong>C 才合理的情境</strong>：確定不要的探索、且確認不需要未來參考</li>
</ul>
<h3 id="d直接刪檔案不留-git-紀錄">D：直接刪檔案、不留 git 紀錄</h3>
<ul>
<li><strong>機制</strong>：手動刪 / 改、不 commit</li>
<li><strong>成本特別高的原因</strong>：未來想比較或恢復都做不到、使用者選項被收窄</li>
<li><strong>D 是反模式</strong>：git 是免費的紀錄工具、不用反而是浪費 — 未來想比較或恢復都做不到、使用者選項被收窄</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該觸發的處理</th>
          <th>第一個該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「先還原」</td>
          <td>確認還原範圍 + checkpoint</td>
          <td>問「還原到哪」</td>
      </tr>
      <tr>
          <td>「重來」</td>
          <td>確認重做起點 + checkpoint</td>
          <td>問「從哪重來」</td>
      </tr>
      <tr>
          <td>「不要做了」</td>
          <td>評估保留價值</td>
          <td>探索成本高就 checkpoint、否則直接刪</td>
      </tr>
      <tr>
          <td>「先放著」</td>
          <td>Stash 或 branch 保留</td>
          <td>不要刪、要留可恢復路徑</td>
      </tr>
      <tr>
          <td>「換個方法試試」</td>
          <td>Checkpoint + 換方向</td>
          <td>保留現方法的進度</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：退出指令的執行不該收窄使用者的未來選項。Checkpoint = 給未來保留可能性、成本只是多一個 commit。</p>
]]></content:encoded></item><item><title>驗證方法的選擇時機</title><link>https://tarrragon.github.io/blog/report/verification-method-timing/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/verification-method-timing/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>驗證工具的引入時機不該等推理徹底失敗。&lt;/strong> 靜態 CSS 推理或視覺截圖溝通連續失敗 ≥ 2 次、立刻主動提「我們啟個 server、我用 playwright 看 live DOM」 — 工具的價值是縮短診斷迴圈、不是最後手段。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼要主動提工具">為什麼要主動提工具&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>執行者堅持靠推理 = 把使用者拖進「截圖 - 反饋 - 再試」的長循環。每輪都消耗使用者時間（看截圖、描述問題、回應）— 對使用者是負擔。&lt;/p>
&lt;p>主動提工具切換 = 把循環從「視覺溝通」改成「程式量測」。執行者直接讀 live DOM、診斷一輪到位、使用者只需要在最終確認。&lt;/p>
&lt;p>主動提的成本是「打一句話建議」、收益是「省 N 輪截圖溝通」。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>drawer 在 form 內、不是 sibling&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>推理 + 寫 CSS + 使用者截圖回報&lt;/td>
 &lt;td>失敗、看不出根因&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>改 CSS + 使用者截圖回報&lt;/td>
 &lt;td>失敗、累積錯誤假設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>加更多覆寫 + 使用者截圖回報&lt;/td>
 &lt;td>失敗、使用者「思路錯了」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>「我啟個 server 看看」&lt;/td>
 &lt;td>立刻發現 drawer 在 form 內&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第 4 輪用 playwright &lt;code>browser_evaluate&lt;/code> 讀 ancestor chain — 一個 query、一個答案、兩分鐘解。前三輪 ≈ 30 分鐘。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>第 2 輪失敗時就應該主動提：&lt;/p>
&lt;blockquote>
&lt;p>「我嘗試了兩次都失敗、根因可能在我對 DOM 結構的假設。要不要啟個 server、我用 playwright 直接讀 live DOM 確認？這樣比繼續用截圖溝通快。」&lt;/p>&lt;/blockquote>
&lt;p>使用者啟 server、我跑 query、一輪解。&lt;/p>
&lt;h3 id="執行主動提工具的-protocol">執行：主動提工具的 protocol&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>推理連續失敗 ≥ 2 次&lt;/td>
 &lt;td>playwright &lt;code>browser_evaluate&lt;/code> 讀 live DOM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不確定元素的真實位置&lt;/td>
 &lt;td>&lt;code>getBoundingClientRect&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不確定 computed style 套到什麼值&lt;/td>
 &lt;td>&lt;code>getComputedStyle&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不確定 framework 渲染後的 DOM&lt;/td>
 &lt;td>playwright snapshot&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不確定跨 viewport 行為&lt;/td>
 &lt;td>playwright 切換 viewport 重測&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>0&lt;/td>
 &lt;td>高 — 截圖、描述、再試&lt;/td>
 &lt;td>有限 — 看截圖看不到 DOM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>瀏覽器 DevTools 手動查&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>中 — 切面板、讀&lt;/td>
 &lt;td>中 — 互動成本高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Playwright &lt;code>browser_evaluate&lt;/code>&lt;/td>
 &lt;td>中 — 起 server&lt;/td>
 &lt;td>低 — 寫一段 evaluate&lt;/td>
 &lt;td>高 — 任意 JS query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Playwright 寫成測試&lt;/td>
 &lt;td>中 — 起 server + 寫測試&lt;/td>
 &lt;td>0 — 自動跑&lt;/td>
 &lt;td>高 + 持續&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「起步成本」是一次性、「每輪成本」是重複的。第 2 輪以後、playwright 的 ROI 已經正向。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>驗證工具的引入時機不該等推理徹底失敗。</strong> 靜態 CSS 推理或視覺截圖溝通連續失敗 ≥ 2 次、立刻主動提「我們啟個 server、我用 playwright 看 live DOM」 — 工具的價值是縮短診斷迴圈、不是最後手段。</p>
<hr>
<h2 id="為什麼要主動提工具">為什麼要主動提工具</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>執行者堅持靠推理 = 把使用者拖進「截圖 - 反饋 - 再試」的長循環。每輪都消耗使用者時間（看截圖、描述問題、回應）— 對使用者是負擔。</p>
<p>主動提工具切換 = 把循環從「視覺溝通」改成「程式量測」。執行者直接讀 live DOM、診斷一輪到位、使用者只需要在最終確認。</p>
<p>主動提的成本是「打一句話建議」、收益是「省 N 輪截圖溝通」。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p><code>drawer 在 form 內、不是 sibling</code> 這個假設錯誤、靠推理 + 截圖溝通走了多輪：</p>
<table>
  <thead>
      <tr>
          <th>輪</th>
          <th>溝通方式</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>推理 + 寫 CSS + 使用者截圖回報</td>
          <td>失敗、看不出根因</td>
      </tr>
      <tr>
          <td>2</td>
          <td>改 CSS + 使用者截圖回報</td>
          <td>失敗、累積錯誤假設</td>
      </tr>
      <tr>
          <td>3</td>
          <td>加更多覆寫 + 使用者截圖回報</td>
          <td>失敗、使用者「思路錯了」</td>
      </tr>
      <tr>
          <td>4</td>
          <td>「我啟個 server 看看」</td>
          <td>立刻發現 drawer 在 form 內</td>
      </tr>
  </tbody>
</table>
<p>第 4 輪用 playwright <code>browser_evaluate</code> 讀 ancestor chain — 一個 query、一個答案、兩分鐘解。前三輪 ≈ 30 分鐘。</p>
<h3 id="判讀">判讀</h3>
<p>第 2 輪失敗時就應該主動提：</p>
<blockquote>
<p>「我嘗試了兩次都失敗、根因可能在我對 DOM 結構的假設。要不要啟個 server、我用 playwright 直接讀 live DOM 確認？這樣比繼續用截圖溝通快。」</p></blockquote>
<p>使用者啟 server、我跑 query、一輪解。</p>
<h3 id="執行主動提工具的-protocol">執行：主動提工具的 protocol</h3>
<p>驗證工具該在這些時機主動提：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該提的工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>推理連續失敗 ≥ 2 次</td>
          <td>playwright <code>browser_evaluate</code> 讀 live DOM</td>
      </tr>
      <tr>
          <td>不確定元素的真實位置</td>
          <td><code>getBoundingClientRect</code></td>
      </tr>
      <tr>
          <td>不確定 computed style 套到什麼值</td>
          <td><code>getComputedStyle</code></td>
      </tr>
      <tr>
          <td>不確定 framework 渲染後的 DOM</td>
          <td>playwright snapshot</td>
      </tr>
      <tr>
          <td>不確定跨 viewport 行為</td>
          <td>playwright 切換 viewport 重測</td>
      </tr>
  </tbody>
</table>
<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>0</td>
          <td>高 — 截圖、描述、再試</td>
          <td>有限 — 看截圖看不到 DOM</td>
      </tr>
      <tr>
          <td>瀏覽器 DevTools 手動查</td>
          <td>0</td>
          <td>中 — 切面板、讀</td>
          <td>中 — 互動成本高</td>
      </tr>
      <tr>
          <td>Playwright <code>browser_evaluate</code></td>
          <td>中 — 起 server</td>
          <td>低 — 寫一段 evaluate</td>
          <td>高 — 任意 JS query</td>
      </tr>
      <tr>
          <td>Playwright 寫成測試</td>
          <td>中 — 起 server + 寫測試</td>
          <td>0 — 自動跑</td>
          <td>高 + 持續</td>
      </tr>
  </tbody>
</table>
<p>「起步成本」是一次性、「每輪成本」是重複的。第 2 輪以後、playwright 的 ROI 已經正向。</p>
<hr>
<h2 id="主動提的具體話術">主動提的具體話術</h2>
<h3 id="較差的提法">較差的提法</h3>
<blockquote>
<p>「要不要試試 playwright」</p></blockquote>
<p>模糊、使用者不一定知道為什麼要試、可能答「先這樣吧」。</p>
<h3 id="較好的提法">較好的提法</h3>
<blockquote>
<p>「我嘗試了兩次都失敗、根因可能不在 CSS、在我對 DOM 結構的假設。
要不要啟個 server（<code>python3 -m http.server 8000</code> 在 public/）、
我用 playwright <code>browser_evaluate</code> 直接讀 ancestor chain 確認？
這樣比繼續用截圖快很多。」</p></blockquote>
<p>說明：</p>
<ul>
<li><strong>為什麼提</strong>：兩次失敗、推理迴圈成本超過工具迴圈</li>
<li><strong>要使用者做什麼</strong>：啟 server、給一行指令</li>
<li><strong>我會做什麼</strong>：用 playwright evaluate 讀</li>
<li><strong>預期收益</strong>：縮短迴圈</li>
</ul>
<p>使用者明確知道 trade-off、決定簡單。</p>
<hr>
<h2 id="工具迴圈的標準流程">工具迴圈的標準流程</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 使用者啟 server（python3 -m http.server / hugo server）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 執行者 navigate 到目標頁面
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 執行者寫 evaluate fn 讀真實狀態
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 執行者根據結果定位根因
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 執行者改 CSS / JS
</span></span><span class="line"><span class="ln">6</span><span class="cl">6. 執行者再 evaluate 驗證修復
</span></span><span class="line"><span class="ln">7</span><span class="cl">7. 使用者目視最後確認（可選）</span></span></code></pre></div><p>整個流程多數步驟在執行者這邊、使用者只在頭尾參與 — 對使用者負擔輕。</p>
<hr>
<h2 id="設計取捨驗證工具引入的時機">設計取捨：驗證工具引入的時機</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（推理 ≥ 2 次失敗主動提）當預設、其他做法在特定情境合理。</p>
<blockquote>
<p>本篇是 <a href="../two-occurrence-threshold/">#42 2 次門檻</a> 抽象原則在「驗證工具切換」這個面向的應用。</p></blockquote>
<h3 id="a推理--2-次失敗主動提工具切換這個專案的預設">A：推理 ≥ 2 次失敗主動提工具切換（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：靜態推理連續失敗 2 次、立刻提「啟個 server、我用 playwright 看 live DOM」+ 附啟用步驟與預期收益</li>
<li><strong>選 A 的理由</strong>：對使用者透明（看到 trade-off）、縮短診斷迴圈</li>
<li><strong>適合</strong>：CSS / DOM 行為跟預期不符的除錯</li>
<li><strong>代價</strong>：執行者要主動辨識「推理迴圈成本」與「工具迴圈成本」的交叉點</li>
</ul>
<h3 id="b等使用者要求才用工具">B：等使用者要求才用工具</h3>
<ul>
<li><strong>機制</strong>：執行者繼續推理、使用者覺得太慢時提</li>
<li><strong>跟 A 的取捨</strong>：B 對使用者更被動、A 主動；B 在使用者不知道有 playwright 選項時、會一直繼續</li>
<li><strong>B 才合理的情境</strong>：使用者明確表達「想用推理練習」、把工具切換當成放棄</li>
</ul>
<h3 id="c全程靜態推理不用工具">C：全程靜態推理、不用工具</h3>
<ul>
<li><strong>機制</strong>：堅持推理到底</li>
<li><strong>C 是反模式</strong>：推理迴圈成本累積、最後可能需要 4-5 輪才解決</li>
<li><strong>看起來吸引人的原因</strong>：覺得用工具是「能力不足」、想撐到自己想出來</li>
<li><strong>實際發生的代價</strong>：時間成本指數放大（每輪推理基於前輪錯假設）、最後還是要切工具</li>
</ul>
<h3 id="d一開始就用-playwright不嘗試推理">D：一開始就用 playwright、不嘗試推理</h3>
<ul>
<li><strong>機制</strong>：跳過推理、直接用工具量</li>
<li><strong>跟 A 的取捨</strong>：D 跳過推理階段省去 2 次嘗試、但前期 setup 成本投入比例較高（簡單問題不值得）</li>
<li><strong>D 比 A 好的情境</strong>：問題明確需要 live DOM 才能診斷（例如「framework 渲染後的結構」）— 推理本來就無法解</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>應該主動提的工具</th>
          <th>提的話術重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>推理 + 截圖溝通 ≥ 2 輪</td>
          <td>playwright <code>browser_evaluate</code></td>
          <td>我假設可能錯、用工具讀 live DOM 確認</td>
      </tr>
      <tr>
          <td>修了 CSS 但使用者截圖看起來沒變</td>
          <td>playwright <code>getComputedStyle</code></td>
          <td>確認 CSS 真的套到、不是 cache 問題</td>
      </tr>
      <tr>
          <td>不確定哪個 viewport 下會有問題</td>
          <td>playwright 多 viewport 測</td>
          <td>一次跑多 viewport、找出哪個壞</td>
      </tr>
      <tr>
          <td>互動狀態下行為不一致</td>
          <td>playwright 模擬互動 + 量測</td>
          <td>自動操作、量結果</td>
      </tr>
      <tr>
          <td>修好了想固化規範</td>
          <td>playwright 寫測試</td>
          <td>把這次發現的契約寫成 expect、未來破壞會被抓</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：工具引入時機是「推理迴圈成本超過工具迴圈成本」的點 — 大多在第 2 次推理失敗時。早一點提、雙方都省時間。</p>
<p>跟 <a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a> 的關係：本卡是「debug 工具切換時機」、#68 是「驗收動作分散在四個時點」 — 兩者共用「動作該分配到哪個時點才有 ROI」這個結構。本卡的「第 2 次推理失敗就切工具」≈ #68 的「ship 前要設計 E2E case」 — 都是「把高 ROI 的動作放在對的時點、不要延後」。</p>
]]></content:encoded></item><item><title>CSS Layers 取代 specificity 戰</title><link>https://tarrragon.github.io/blog/report/css-layers-over-specificity/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/css-layers-over-specificity/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>CSS Layers 把樣式覆寫從「線性 specificity 數字戰」改成「分組權重順序」。&lt;/strong> 把外部組件 CSS &lt;code>@import&lt;/code> 進一個 layer、自家 CSS 留在 unlayered，自家規則自動贏 — 不論個別 selector specificity 數值。一次設定、所有 &lt;code>!important&lt;/code> 與 &lt;code>.x.x&lt;/code> 雙寫 hack 可以拿掉。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-specificity-戰沒有贏家">為什麼 specificity 戰沒有贏家&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS specificity 是線性數字比較。組件作者用 &lt;code>.x.svelte-yyy.svelte-yyy&lt;/code> 雙寫 specificity 30 → 自家用 &lt;code>.search-shell .x&lt;/code> specificity 20 蓋不過 → 加 &lt;code>.x.x&lt;/code> 雙寫到 30 → 還是看 source order → 加 &lt;code>!important&lt;/code> → 跟其他 important 對撞 → 寫死多層 fallback。&lt;/p>
&lt;p>每加一層覆寫成本累積、未來 debug 越來越難。每個 &lt;code>!important&lt;/code> 都是一個 future debugging burden、&lt;code>!important&lt;/code> 之間沒有層級可言。&lt;/p>
&lt;h3 id="css-layers-的解法">CSS Layers 的解法&lt;/h3>
&lt;p>CSS &lt;code>@layer&lt;/code> 提供「分組權重」 — unlayered CSS &amp;gt; layered CSS（layer 越早宣告越低權）、跟 selector specificity 無關：&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">unlayered { ... } ← 最高權
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">@layer high { ... }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">@layer medium { ... }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">@layer low { ... } ← 最低權&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把組件 CSS 整包丟進某個 layer、自家 CSS 留 unlayered、自家規則自動贏所有組件規則 — &lt;strong>不論 specificity&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的覆寫戰場">這次任務的覆寫戰場&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>現在 &lt;code>search.html&lt;/code> 內為了蓋過 pagefind specificity 30 的寫法：&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="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-block&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">border-bottom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="cp">!important&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">2&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-panel&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&lt;/span> &lt;span class="cp">!important&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="p">.&lt;/span>&lt;span class="nc">search-filter-slot&lt;/span> &lt;span class="nt">fieldset&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">border&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">margin&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&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>每條都靠 &lt;code>!important&lt;/code> 或 source order 取勝。可維護性低。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 pagefind-ui.css 用 &lt;code>@import&lt;/code> 包進 layer：&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="p">@&lt;/span>&lt;span class="k">import&lt;/span> &lt;span class="nt">url&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;/blog/pagefind/pagefind-ui.css&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="nt">layer&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nt">pagefind&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>自家 CSS 不加 layer 宣告、留 unlayered。自家規則優先級自動高於 layer(pagefind)。&lt;/p>
&lt;h3 id="執行refactor-步驟">執行：refactor 步驟&lt;/h3>





&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">/* search.html / assets/search.css */&lt;/span>
&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 class="c">/* 把 pagefind 的整包 CSS 包進 layer */&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="k">import&lt;/span> &lt;span class="nt">url&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;/blog/pagefind/pagefind-ui.css&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="nt">layer&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nt">pagefind&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c">/* 自家 CSS 留 unlayered、自動贏 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-block&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">border-bottom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 不需要 !important */&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-panel&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="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 不需要 !important */&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;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">@&lt;/span>&lt;span class="k">media&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nt">min-width&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nt">1400px&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nc">pagefind-ui__filter-panel&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&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">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>原本的 &lt;code>&amp;lt;link href=&amp;quot;...pagefind-ui.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot;&amp;gt;&lt;/code> 改成上方 &lt;code>@import&lt;/code> 寫法、確保 import 在自家 CSS 之前發生（layered CSS 不會阻擋 unlayered CSS 的優先級）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>CSS Layers 把樣式覆寫從「線性 specificity 數字戰」改成「分組權重順序」。</strong> 把外部組件 CSS <code>@import</code> 進一個 layer、自家 CSS 留在 unlayered，自家規則自動贏 — 不論個別 selector specificity 數值。一次設定、所有 <code>!important</code> 與 <code>.x.x</code> 雙寫 hack 可以拿掉。</p>
<hr>
<h2 id="為什麼-specificity-戰沒有贏家">為什麼 specificity 戰沒有贏家</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS specificity 是線性數字比較。組件作者用 <code>.x.svelte-yyy.svelte-yyy</code> 雙寫 specificity 30 → 自家用 <code>.search-shell .x</code> specificity 20 蓋不過 → 加 <code>.x.x</code> 雙寫到 30 → 還是看 source order → 加 <code>!important</code> → 跟其他 important 對撞 → 寫死多層 fallback。</p>
<p>每加一層覆寫成本累積、未來 debug 越來越難。每個 <code>!important</code> 都是一個 future debugging burden、<code>!important</code> 之間沒有層級可言。</p>
<h3 id="css-layers-的解法">CSS Layers 的解法</h3>
<p>CSS <code>@layer</code> 提供「分組權重」 — unlayered CSS &gt; layered CSS（layer 越早宣告越低權）、跟 selector specificity 無關：</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">unlayered { ... }              ← 最高權
</span></span><span class="line"><span class="ln">2</span><span class="cl">@layer high { ... }
</span></span><span class="line"><span class="ln">3</span><span class="cl">@layer medium { ... }
</span></span><span class="line"><span class="ln">4</span><span class="cl">@layer low { ... }             ← 最低權</span></span></code></pre></div><p>把組件 CSS 整包丟進某個 layer、自家 CSS 留 unlayered、自家規則自動贏所有組件規則 — <strong>不論 specificity</strong>。</p>
<hr>
<h2 id="這次任務的覆寫戰場">這次任務的覆寫戰場</h2>
<h3 id="觀察">觀察</h3>
<p>現在 <code>search.html</code> 內為了蓋過 pagefind specificity 30 的寫法：</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="p">.</span><span class="nc">pagefind-ui__filter-block</span> <span class="p">{</span> <span class="k">border-bottom</span><span class="p">:</span> <span class="mi">0</span> <span class="cp">!important</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__filter-panel</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span> <span class="cp">!important</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-filter-slot</span> <span class="nt">fieldset</span> <span class="p">{</span> <span class="k">border</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>每條都靠 <code>!important</code> 或 source order 取勝。可維護性低。</p>
<h3 id="判讀">判讀</h3>
<p>把 pagefind-ui.css 用 <code>@import</code> 包進 layer：</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="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/blog/pagefind/pagefind-ui.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">pagefind</span><span class="o">)</span><span class="p">;</span></span></span></code></pre></div><p>自家 CSS 不加 layer 宣告、留 unlayered。自家規則優先級自動高於 layer(pagefind)。</p>
<h3 id="執行refactor-步驟">執行：refactor 步驟</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">/* search.html / assets/search.css */</span>
</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="c">/* 把 pagefind 的整包 CSS 包進 layer */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/blog/pagefind/pagefind-ui.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">pagefind</span><span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c">/* 自家 CSS 留 unlayered、自動贏 */</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__filter-block</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">border-bottom</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>            <span class="c">/* 不需要 !important */</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__filter-panel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>               <span class="c">/* 不需要 !important */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">@</span><span class="k">media</span> <span class="o">(</span><span class="nt">min-width</span><span class="o">:</span> <span class="nt">1400px</span><span class="o">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">.</span><span class="nc">pagefind-ui__filter-panel</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>原本的 <code>&lt;link href=&quot;...pagefind-ui.css&quot; rel=&quot;stylesheet&quot;&gt;</code> 改成上方 <code>@import</code> 寫法、確保 import 在自家 CSS 之前發生（layered CSS 不會阻擋 unlayered CSS 的優先級）。</p>
<hr>
<h2 id="內在屬性比較四種-specificity-應對">內在屬性比較：四種 specificity 應對</h2>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>維護成本</th>
          <th>可讀性</th>
          <th>升級兼容性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>!important</code> 對抗</td>
          <td>高 — 每加一條未來 debug 成本上升</td>
          <td>低 — 不知為什麼要 important</td>
          <td>中 — 組件變更可能讓 important 用錯</td>
      </tr>
      <tr>
          <td>雙寫 class（<code>.x.x</code>）</td>
          <td>中 — selector 看起來奇怪</td>
          <td>低 — 維護者不知為什麼</td>
          <td>中 — 組件改 class 名就失效</td>
      </tr>
      <tr>
          <td>Inline style + setProperty important</td>
          <td>高 — 散落在 JS 各處</td>
          <td>最低 — 不在 CSS 找不到</td>
          <td>低 — JS 規則容易被 framework 重繪打破</td>
      </tr>
      <tr>
          <td>CSS Layers</td>
          <td>低 — 一次設定、規則簡單</td>
          <td>高 — 結構化分層</td>
          <td>高 — 跟組件升級無關</td>
      </tr>
  </tbody>
</table>
<p><strong>Layers 的所有指標都最佳</strong>。其他三種是 Layers 之前的 workaround、現在沒理由繼續用。</p>
<hr>
<h2 id="layers-的進階用法">Layers 的進階用法</h2>
<h3 id="多個外部組件分別-layer">多個外部組件分別 layer</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="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;vendor-a.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">vendor-a</span><span class="o">)</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;vendor-b.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">vendor-b</span><span class="o">)</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="p">@</span><span class="k">layer</span> <span class="nt">vendor-a</span><span class="o">,</span> <span class="nt">vendor-b</span><span class="p">;</span>   <span class="c">/* 後宣告的優先 */</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c">/* 自家 unlayered */</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">.</span><span class="nc">my-overrides</span> <span class="p">{</span> <span class="err">...</span> <span class="p">}</span></span></span></code></pre></div><p><code>@layer name1, name2;</code> 顯式宣告 layer 順序、後宣告的權重高。</p>
<h3 id="自家-css-也分層">自家 CSS 也分層</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="p">@</span><span class="k">layer</span> <span class="nt">base</span><span class="o">,</span> <span class="nt">components</span><span class="o">,</span> <span class="nt">utilities</span><span class="p">;</span>
</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="p">@</span><span class="k">layer</span> <span class="nt">base</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">body</span> <span class="p">{</span> <span class="k">font-family</span><span class="p">:</span> <span class="o">...</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="k">layer</span> <span class="nt">components</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">.</span><span class="nc">button</span> <span class="p">{</span> <span class="k">padding</span><span class="p">:</span> <span class="o">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">@</span><span class="k">layer</span> <span class="nt">utilities</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">.</span><span class="nc">text-center</span> <span class="p">{</span> <span class="k">text-align</span><span class="p">:</span> <span class="kc">center</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>自家 CSS 內部也分層、避免 utilities 被 components 蓋過。</p>
<h3 id="跟-unlayered-並存">跟 unlayered 並存</h3>
<p>不是所有自家 CSS 都要分 layer。<strong>最高優先的自家規則留 unlayered、其他規則可以分層</strong>。</p>
<hr>
<h2 id="瀏覽器支援">瀏覽器支援</h2>
<p>CSS Layers 在所有主流瀏覽器（Chrome 99+、Firefox 97+、Safari 15.4+）支援、2022 年起。當前（2026）所有現代瀏覽器都支援。</p>
<p>對舊瀏覽器降級：不支援 <code>@layer</code> 的瀏覽器會把整個 <code>@layer { ... }</code> block 當作 invalid 跳過 — 自家 unlayered 規則仍然適用、效果一樣（但 vendor CSS 完全失效）。實務上不需要擔心。</p>
<hr>
<h2 id="設計取捨覆寫外部組件-css-的策略">設計取捨：覆寫外部組件 CSS 的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（CSS Layers）當預設、其他做法在特定情境合理。</p>
<h3 id="acss-layers這個專案的預設">A：CSS Layers（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>@import url(...) layer(vendor)</code> 把外部 CSS 包進低權層、自家 unlayered CSS 自動贏</li>
<li><strong>選 A 的理由</strong>：跨組件升級穩定、規則簡單、<code>!important</code> 完全不需要、跳出 specificity 線性比較戰場</li>
<li><strong>適合</strong>：所有現代瀏覽器（Chrome 99+ / Firefox 97+ / Safari 15.4+）的客製情境</li>
<li><strong>代價</strong>：需要重新引入 vendor CSS（從 <code>&lt;link&gt;</code> 改 <code>@import</code>）</li>
</ul>
<h3 id="b雙寫-class-提升-specificity">B：雙寫 class 提升 specificity</h3>
<ul>
<li><strong>機制</strong>：<code>.pagefind-ui__filter-block.pagefind-ui__filter-block</code> 寫兩次提升 specificity 從 10 到 20</li>
<li><strong>跟 A 的取捨</strong>：B 不需要改 vendor CSS 引入方式、A 需要；但 B 跟組件 specificity 競賽（組件作者改 hash 寫法就壞）、A 跳出競賽</li>
<li><strong>B 是反模式</strong>：跟組件 specificity 競賽（組件作者改 hash 寫法就壞） — 唯一例外是 vendor CSS 不能用 <code>@import</code> 引入（極罕見的 build pipeline 限制）</li>
</ul>
<h3 id="cimportant-對抗">C：<code>!important</code> 對抗</h3>
<ul>
<li><strong>機制</strong>：每條覆寫加 <code>!important</code>、用 importance 取勝</li>
<li><strong>跟 A 的取捨</strong>：C 短期有效、長期 important 之間沒層級可言；多個 important 對撞時 debug 困難</li>
<li><strong>C 才合理的情境</strong>：CSS Layers 不支援的舊瀏覽器（&lt; 2022 的版本）、且確認沒其他 important 對撞</li>
</ul>
<h3 id="dinline-style--setpropertyimportant">D：Inline style + <code>setProperty('important')</code></h3>
<ul>
<li><strong>機制</strong>：JS 用 <code>el.style.setProperty('display', 'none', 'important')</code></li>
<li><strong>成本特別高的原因</strong>：規則散落在 JS 各處、devtools 看不出意圖、跟 framework 重繪競爭</li>
<li><strong>D 才合理的情境</strong>：動態值（runtime 算的位置 / 尺寸）必須用 inline 表達 — 但即使這樣、也建議用 class toggle + CSS 變數（<a href="../class-toggle-over-important/">#28</a>）取代</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>為了蓋過組件規則寫了 <code>!important</code></td>
          <td>評估改用 CSS Layers</td>
      </tr>
      <tr>
          <td>Selector 寫成 <code>.x.x</code> 雙寫只為了 specificity</td>
          <td>評估改用 CSS Layers</td>
      </tr>
      <tr>
          <td>覆寫邏輯散落在多個檔案 / inline style</td>
          <td>集中到一份 CSS、用 layers 分層</td>
      </tr>
      <tr>
          <td>組件升級後覆寫失效</td>
          <td>用 layers 隔離、跟組件 specificity 變動脫鉤</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：跟組件 CSS 競爭 specificity 是不必要的戰爭。Layers 提供更高層的權重機制、把覆寫簡化成「自家 vs 別人」的二元決定。</p>
]]></content:encoded></item><item><title>CSS / JS 拆出獨立檔案</title><link>https://tarrragon.github.io/blog/report/extract-css-js-files/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/extract-css-js-files/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Inline CSS / JS 超過 ~30 行就值得拆出獨立檔案、走 Hugo &lt;code>resources.Get | minify | fingerprint&lt;/code> 引入。&lt;/strong> Template 變單純、editor 對 .css/.js 有 syntax highlight、minify 自動化、cache-busting fingerprint 自動處理。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-inline-有上限">為什麼 inline 有上限&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Inline CSS / JS 在 Hugo template 內看似省事（一個檔案搞定），但隨著規模上升出現多個成本：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>規模&lt;/th>
 &lt;th>Inline 的代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&amp;lt; 10 行&lt;/td>
 &lt;td>幾乎無 — 一目了然&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10-30 行&lt;/td>
 &lt;td>中 — Editor 不太能 highlight、template 開始混雜&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>30+ 行&lt;/td>
 &lt;td>高 — 找東西要在 template 模式間切換、minify 沒做、cache 控制困難&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>拆檔的成本是「多 1-2 個檔案」、收益是「multiple」 — 過了 30 行門檻、ROI 已正向。&lt;/p>
&lt;h3 id="拆檔的實際得益">拆檔的實際得益&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Inline&lt;/th>
 &lt;th>拆檔 + Resources Pipeline&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Editor syntax highlight&lt;/td>
 &lt;td>部分 — 看 editor 是否支援 mixed mode&lt;/td>
 &lt;td>完整 — 純 .css / .js 檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Minify&lt;/td>
 &lt;td>手動或 hugo template minify&lt;/td>
 &lt;td>Hugo &lt;code>minify&lt;/code> pipe 自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache-busting&lt;/td>
 &lt;td>手動加版本號&lt;/td>
 &lt;td>&lt;code>fingerprint&lt;/code> 自動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式碼重用&lt;/td>
 &lt;td>難 — 跟 template 綁&lt;/td>
 &lt;td>容易 — 多 template 共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Version control diff&lt;/td>
 &lt;td>跟 template 改動混&lt;/td>
 &lt;td>純檔案改動、清楚&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試&lt;/td>
 &lt;td>難&lt;/td>
 &lt;td>可單獨測&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="這次任務的拆檔目標">這次任務的拆檔目標&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>layouts/_default/search.html&lt;/code> 現況：&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>Hugo template 與 HTML&lt;/td>
 &lt;td>~30&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Inline &lt;code>&amp;lt;script&amp;gt;&lt;/code>&lt;/td>
 &lt;td>~110&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Inline &lt;code>&amp;lt;style&amp;gt;&lt;/code>&lt;/td>
 &lt;td>~80&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>總計&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>~220&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>220 行的 single-file template、CSS / JS 各超過拆檔門檻 3-4 倍。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 CSS 拆到 &lt;code>assets/search.css&lt;/code>、JS 拆到 &lt;code>assets/search.js&lt;/code>、template 只剩 HTML 結構與 Hugo 引入。&lt;/p>
&lt;h3 id="執行拆檔步驟">執行：拆檔步驟&lt;/h3>
&lt;h4 id="step-1建立-assets-檔">Step 1：建立 assets 檔&lt;/h4>





&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">assets/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── search.css # 原本 inline &amp;lt;style&amp;gt; 內容
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">└── search.js # 原本 inline &amp;lt;script&amp;gt; 內容&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-2template-引入">Step 2：template 引入&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">{{ define &amp;#34;main&amp;#34; }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">{{- $css := resources.Get &amp;#34;search.css&amp;#34; | minify | fingerprint -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">link&lt;/span> &lt;span class="na">href&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $css.RelPermalink }}&amp;#34;&lt;/span> &lt;span class="na">rel&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;stylesheet&amp;#34;&lt;/span> &lt;span class="na">integrity&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $css.Data.Integrity }}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&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 class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">data-pagefind-ignore&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-shell&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&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 class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">{{- $js := resources.Get &amp;#34;search.js&amp;#34; | minify | fingerprint -}}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">src&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $js.RelPermalink }}&amp;#34;&lt;/span> &lt;span class="na">integrity&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ $js.Data.Integrity }}&amp;#34;&lt;/span> &lt;span class="na">defer&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">{{ end }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="step-3js-從全域-windowpagefindui-改為-module-模式">Step 3：JS 從全域 &lt;code>window.PagefindUI&lt;/code> 改為 module 模式&lt;/h4>
&lt;p>如果原本 inline JS 用 &lt;code>new PagefindUI(...)&lt;/code> 直接執行、拆檔後仍然可以這樣寫。但若想進一步，把 init 包成 function：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Inline CSS / JS 超過 ~30 行就值得拆出獨立檔案、走 Hugo <code>resources.Get | minify | fingerprint</code> 引入。</strong> Template 變單純、editor 對 .css/.js 有 syntax highlight、minify 自動化、cache-busting fingerprint 自動處理。</p>
<hr>
<h2 id="為什麼-inline-有上限">為什麼 inline 有上限</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Inline CSS / JS 在 Hugo template 內看似省事（一個檔案搞定），但隨著規模上升出現多個成本：</p>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>Inline 的代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&lt; 10 行</td>
          <td>幾乎無 — 一目了然</td>
      </tr>
      <tr>
          <td>10-30 行</td>
          <td>中 — Editor 不太能 highlight、template 開始混雜</td>
      </tr>
      <tr>
          <td>30+ 行</td>
          <td>高 — 找東西要在 template 模式間切換、minify 沒做、cache 控制困難</td>
      </tr>
  </tbody>
</table>
<p>拆檔的成本是「多 1-2 個檔案」、收益是「multiple」 — 過了 30 行門檻、ROI 已正向。</p>
<h3 id="拆檔的實際得益">拆檔的實際得益</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Inline</th>
          <th>拆檔 + Resources Pipeline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Editor syntax highlight</td>
          <td>部分 — 看 editor 是否支援 mixed mode</td>
          <td>完整 — 純 .css / .js 檔</td>
      </tr>
      <tr>
          <td>Minify</td>
          <td>手動或 hugo template minify</td>
          <td>Hugo <code>minify</code> pipe 自動</td>
      </tr>
      <tr>
          <td>Cache-busting</td>
          <td>手動加版本號</td>
          <td><code>fingerprint</code> 自動</td>
      </tr>
      <tr>
          <td>程式碼重用</td>
          <td>難 — 跟 template 綁</td>
          <td>容易 — 多 template 共用</td>
      </tr>
      <tr>
          <td>Version control diff</td>
          <td>跟 template 改動混</td>
          <td>純檔案改動、清楚</td>
      </tr>
      <tr>
          <td>測試</td>
          <td>難</td>
          <td>可單獨測</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="這次任務的拆檔目標">這次任務的拆檔目標</h2>
<h3 id="觀察">觀察</h3>
<p><code>layouts/_default/search.html</code> 現況：</p>
<table>
  <thead>
      <tr>
          <th>段落</th>
          <th>行數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hugo template 與 HTML</td>
          <td>~30</td>
      </tr>
      <tr>
          <td>Inline <code>&lt;script&gt;</code></td>
          <td>~110</td>
      </tr>
      <tr>
          <td>Inline <code>&lt;style&gt;</code></td>
          <td>~80</td>
      </tr>
      <tr>
          <td><strong>總計</strong></td>
          <td><strong>~220</strong></td>
      </tr>
  </tbody>
</table>
<p>220 行的 single-file template、CSS / JS 各超過拆檔門檻 3-4 倍。</p>
<h3 id="判讀">判讀</h3>
<p>把 CSS 拆到 <code>assets/search.css</code>、JS 拆到 <code>assets/search.js</code>、template 只剩 HTML 結構與 Hugo 引入。</p>
<h3 id="執行拆檔步驟">執行：拆檔步驟</h3>
<h4 id="step-1建立-assets-檔">Step 1：建立 assets 檔</h4>





<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">assets/
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── search.css      # 原本 inline &lt;style&gt; 內容
</span></span><span class="line"><span class="ln">3</span><span class="cl">└── search.js       # 原本 inline &lt;script&gt; 內容</span></span></code></pre></div><h4 id="step-2template-引入">Step 2：template 引入</h4>





<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">{{ define &#34;main&#34; }}
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">{{- $css := resources.Get &#34;search.css&#34; | minify | fingerprint -}}
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{ $css.RelPermalink }}&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;{{ $css.Data.Integrity }}&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">data-pagefind-ignore</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-shell&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  ...
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</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">{{- $js := resources.Get &#34;search.js&#34; | minify | fingerprint -}}
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;{{ $js.RelPermalink }}&#34;</span> <span class="na">integrity</span><span class="o">=</span><span class="s">&#34;{{ $js.Data.Integrity }}&#34;</span> <span class="na">defer</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">{{ end }}</span></span></code></pre></div><h4 id="step-3js-從全域-windowpagefindui-改為-module-模式">Step 3：JS 從全域 <code>window.PagefindUI</code> 改為 module 模式</h4>
<p>如果原本 inline JS 用 <code>new PagefindUI(...)</code> 直接執行、拆檔後仍然可以這樣寫。但若想進一步，把 init 包成 function：</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">// assets/search.js
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">function</span> <span class="nx">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">new</span> <span class="nx">PagefindUI</span><span class="p">({</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1">// ... rest of setup
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="s1">&#39;loading&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="nx">init</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">init</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">})();</span></span></span></code></pre></div><h4 id="step-4清理-template">Step 4：清理 template</h4>
<p>Template 從 220 行降到 ~30 行 — 只剩 HTML 結構。</p>
<hr>
<h2 id="內在屬性比較四種引入方式">內在屬性比較：四種引入方式</h2>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>維護成本</th>
          <th>Cache 控制</th>
          <th>可重用性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Inline <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code></td>
          <td>中 — template 混雜</td>
          <td>自動跟著 template</td>
          <td>低 — 跟特定 template 綁</td>
      </tr>
      <tr>
          <td>拆 .css / .js + 直接 link / script tag</td>
          <td>低 — 純檔案</td>
          <td>手動加版本號</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Hugo resources.Get + minify</td>
          <td>低</td>
          <td>內容變動觸發新 path</td>
          <td>高</td>
      </tr>
      <tr>
          <td>Hugo resources.Get + minify + fingerprint</td>
          <td>低</td>
          <td>內容 hash 自動 cache-bust</td>
          <td>高 + 安全</td>
      </tr>
  </tbody>
</table>
<p>優先選 fingerprint — Hugo 自動處理快取、瀏覽器看到內容變動的 fingerprint 一定 reload。</p>
<hr>
<h2 id="hugo-resources-pipeline-的細節">Hugo Resources Pipeline 的細節</h2>
<h3 id="resourcesget"><code>resources.Get</code></h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{{</span> <span class="err">$</span><span class="nx">css</span> <span class="o">:=</span> <span class="nx">resources</span><span class="p">.</span><span class="nx">Get</span> <span class="s">&#34;search.css&#34;</span> <span class="p">}}</span></span></span></code></pre></div><p>讀 <code>assets/search.css</code>。如果路徑下沒有、回傳 nil（要做 nil 檢查）。</p>
<h3 id="-minify"><code>| minify</code></h3>
<p>去除空白、註解、合併 selector — 減少傳輸大小。</p>
<h3 id="-fingerprint"><code>| fingerprint</code></h3>
<p>對檔案內容做 hash、加到 URL（<code>search.abc123.css</code>）。內容變動時 fingerprint 變、瀏覽器把它當新檔案。</p>
<h3 id="relpermalink--permalink"><code>.RelPermalink</code> / <code>.Permalink</code></h3>
<p><code>RelPermalink</code> — site root 相對路徑（<code>/search.abc123.css</code>）<br>
<code>Permalink</code> — 完整 URL（<code>https://site.com/search.abc123.css</code>）</p>
<p>通常用 <code>RelPermalink</code> 即可。</p>
<h3 id="dataintegrity"><code>.Data.Integrity</code></h3>
<p>Subresource Integrity hash — 給 <code>integrity</code> attribute 用、瀏覽器驗證下載內容沒被篡改。</p>
<hr>
<h2 id="拆檔的判斷門檻">拆檔的判斷門檻</h2>
<table>
  <thead>
      <tr>
          <th>Template 內含</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0-10 行 inline CSS / JS</td>
          <td>不拆 — 維護成本最低</td>
      </tr>
      <tr>
          <td>10-30 行</td>
          <td>視情況 — 有重用性需求就拆</td>
      </tr>
      <tr>
          <td>30+ 行</td>
          <td>拆 — 各方面收益都正向</td>
      </tr>
      <tr>
          <td>50+ 行</td>
          <td>強烈建議拆</td>
      </tr>
      <tr>
          <td>多個 template 共用同一段</td>
          <td>立刻拆 — 重用性主導</td>
      </tr>
  </tbody>
</table>
<p>當前 search.html 的 ~190 行 inline 程式碼遠超門檻、屬於「強烈建議拆」。</p>
<hr>
<h2 id="設計取捨css--js-引入策略">設計取捨：CSS / JS 引入策略</h2>
<p>四種做法、各自機會成本不同。這個專案在 inline &gt; 30 行時選 A（拆檔 + Hugo pipeline）當預設、其他做法在特定情境合理。</p>
<h3 id="a拆檔--hugo-resourcesget--minify--fingerprint這個專案的預設">A：拆檔 + Hugo <code>resources.Get | minify | fingerprint</code>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：CSS / JS 拆到 <code>assets/</code>、template 用 <code>resources.Get | minify | fingerprint</code> 引入</li>
<li><strong>選 A 的理由</strong>：minify 自動、cache-bust 自動、editor syntax highlight、跨 template 重用</li>
<li><strong>適合</strong>：規模超過 30 行、預期長期維護的客製</li>
<li><strong>代價</strong>：多 1-2 個檔案、template 跟 assets 分屬兩處（grep 多一步）</li>
</ul>
<h3 id="b拆檔--直接-link--script-tag">B：拆檔 + 直接 <code>&lt;link&gt;</code> / <code>&lt;script&gt;</code> tag</h3>
<ul>
<li><strong>機制</strong>：拆檔到 <code>static/</code> 或 <code>assets/</code>、template 直接 link</li>
<li><strong>跟 A 的取捨</strong>：B 簡單、A 自動處理 minify / fingerprint；B 改檔案後 cache 可能用舊版（要手動加版本號）</li>
<li><strong>B 比 A 好的情境</strong>：簡單 prototype、確定不需要 cache-bust（純內部工具）</li>
</ul>
<h3 id="c保持-inline">C：保持 inline</h3>
<ul>
<li><strong>機制</strong>：CSS / JS 寫在 template 的 <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code> 內</li>
<li><strong>跟 A 的取捨</strong>：C 一個檔案搞定、A 拆兩個；但 C 在 30+ 行時 syntax highlight 失效、難維護</li>
<li><strong>C 比 A 好的情境</strong>：&lt; 10 行的小段、跟 template 邏輯緊密相關</li>
</ul>
<h3 id="dcdn-引入第三方資源">D：CDN 引入第三方資源</h3>
<ul>
<li><strong>機制</strong>：<code>&lt;script src=&quot;https://cdn.../lib.js&quot;&gt;</code></li>
<li><strong>成本特別高的原因</strong>：依賴第三方可用性、跨域 CORS / SRI 處理、隱私問題（追蹤）</li>
<li><strong>D 才合理的情境</strong>：第三方明確支援 SRI 且 CDN 是官方建議方式（少數 vendor library）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>拆檔動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Template 內 <code>&lt;style&gt;</code> / <code>&lt;script&gt;</code> 超過 30 行</td>
          <td>拆到 <code>assets/</code> 下對應 .css / .js</td>
      </tr>
      <tr>
          <td>Editor 對 inline CSS / JS 沒 highlight</td>
          <td>拆檔讓 editor 套對應 mode</td>
      </tr>
      <tr>
          <td>改 inline JS 後 cache 沒更新</td>
          <td>拆檔 + fingerprint 自動 cache-bust</td>
      </tr>
      <tr>
          <td>同樣的 CSS / JS 在多個 template 重複</td>
          <td>拆出共用檔案</td>
      </tr>
      <tr>
          <td>Inline 程式碼跟 Hugo template 邏輯混在一起難 grep</td>
          <td>拆檔讓 grep 範圍清楚</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Template 是 markup 的家、CSS / JS 是各自獨立檔案的家。三者混在一個檔案是過渡狀態、不是長期方案。</p>
]]></content:encoded></item><item><title>CSS 變數定義位置統一</title><link>https://tarrragon.github.io/blog/report/css-variable-single-location/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/css-variable-single-location/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>CSS 變數的定義位置只能有一處。&lt;/strong> 一次定義在離 root 最近的合適 selector（&lt;code>:root&lt;/code> 或頁面層級的 body class），其他地方只用 &lt;code>var()&lt;/code> 引用、不重複宣告。改 token 只動一處、所有引用點自動跟上。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼定義位置要單一">為什麼定義位置要單一&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS 變數的價值是「單一來源、多處引用」。把定義散在多個 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="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">2&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">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&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;/code>&lt;/pre>&lt;/div>&lt;p>每個變數的「真相」分散在不同位置 — 改一個 token 要先 grep 找到定義位置、可能漏改。&lt;/p>
&lt;p>更嚴重：同名變數在不同 selector 重複定義時、值依 cascade 順序決定 — 維護者不易看出哪個值生效。&lt;/p>
&lt;h3 id="統一定義的位置選擇">統一定義的位置選擇&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>影響範圍&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>:root&lt;/code>&lt;/td>
 &lt;td>全站適用的 design token&lt;/td>
 &lt;td>全站&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>body.page-X&lt;/code>&lt;/td>
 &lt;td>特定頁面類型適用&lt;/td>
 &lt;td>該類型頁面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>.component-name&lt;/code>&lt;/td>
 &lt;td>特定 component 內適用&lt;/td>
 &lt;td>該 component 子樹&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇原則：&lt;strong>定義在「跟使用範圍最匹配的最高層級」selector&lt;/strong>。全站用 &lt;code>:root&lt;/code>、頁面類型用 body class、組件內用組件 class。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的散落問題">這次任務的散落問題&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>search.html&lt;/code> 內 CSS 變數定義散在三處：&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="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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* JS 量測會覆寫 */&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&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>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三處定義 — 雖然各有理由（body 範圍、JS 寫入點、cascade 給 pagefind），但維護者要知道「改 search-form-h 在哪改」需要全文 grep。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>整理後集中在 &lt;code>body.page-search&lt;/code>（搜尋頁的 root 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="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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="c">/* 設計 token：寫死值 */&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>&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>&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>&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 class="c">/* JS 量測寫入 fallback：JS 會用 setProperty 覆寫到 :root */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="c">/* 給 pagefind cascade 的 scale */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&lt;/span>&lt;span class="p">;&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>一個 selector 看到所有 search 相關 token、cascade 到子樹生效。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>
&lt;p>JS 量測寫入 scope-h 時、寫到 &lt;code>body.page-search&lt;/code> 而非 &lt;code>:root&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">56&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="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&amp;#39;&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寫到 body.style 直接覆蓋 body.page-search 的 fallback 值。Cascade 到所有後代生效。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>--token-*&lt;/code> 或無前綴&lt;/td>
 &lt;td>全站設計 token（顏色、字型）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--page-search-*&lt;/code>&lt;/td>
 &lt;td>搜尋頁專用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--pagefind-ui-*&lt;/code>&lt;/td>
 &lt;td>Pagefind 提供的 hook（不是我們命名、是組件預期）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>前綴讓維護者一眼看出變數的「歸屬」、不會誤改別處變數。&lt;/p>
&lt;h3 id="分類定義">分類定義&lt;/h3>





&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="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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="c">/* === 對齊 token === */&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>&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>&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* JS 寫入 */&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">/* === 響應式 breakpoint === */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c">/* (CSS 變數無法用在 @media query、breakpoint 寫死在 query 內) */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="c">/* === 對組件的 hook === */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分類註解讓維護者知道「我要改哪類 token」、找對位置。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>CSS 變數的定義位置只能有一處。</strong> 一次定義在離 root 最近的合適 selector（<code>:root</code> 或頁面層級的 body class），其他地方只用 <code>var()</code> 引用、不重複宣告。改 token 只動一處、所有引用點自動跟上。</p>
<hr>
<h2 id="為什麼定義位置要單一">為什麼定義位置要單一</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS 變數的價值是「單一來源、多處引用」。把定義散在多個 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="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">2</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span>   <span class="p">{</span> <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</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></code></pre></div><p>每個變數的「真相」分散在不同位置 — 改一個 token 要先 grep 找到定義位置、可能漏改。</p>
<p>更嚴重：同名變數在不同 selector 重複定義時、值依 cascade 順序決定 — 維護者不易看出哪個值生效。</p>
<h3 id="統一定義的位置選擇">統一定義的位置選擇</h3>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>適用情境</th>
          <th>影響範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>:root</code></td>
          <td>全站適用的 design token</td>
          <td>全站</td>
      </tr>
      <tr>
          <td><code>body.page-X</code></td>
          <td>特定頁面類型適用</td>
          <td>該類型頁面</td>
      </tr>
      <tr>
          <td><code>.component-name</code></td>
          <td>特定 component 內適用</td>
          <td>該 component 子樹</td>
      </tr>
  </tbody>
</table>
<p>選擇原則：<strong>定義在「跟使用範圍最匹配的最高層級」selector</strong>。全站用 <code>:root</code>、頁面類型用 body class、組件內用組件 class。</p>
<hr>
<h2 id="這次任務的散落問題">這次任務的散落問題</h2>
<h3 id="觀察">觀察</h3>
<p><code>search.html</code> 內 CSS 變數定義散在三處：</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="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-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"> 3</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"> 4</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></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></span><span class="line"><span class="ln"> 7</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"> 8</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">/* JS 量測會覆寫 */</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">.</span><span class="nc">search-shell</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>三處定義 — 雖然各有理由（body 範圍、JS 寫入點、cascade 給 pagefind），但維護者要知道「改 search-form-h 在哪改」需要全文 grep。</p>
<h3 id="判讀">判讀</h3>
<p>整理後集中在 <code>body.page-search</code>（搜尋頁的 root 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="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">/* 設計 token：寫死值 */</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="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c">/* JS 量測寫入 fallback：JS 會用 setProperty 覆寫到 :root */</span>
</span></span><span class="line"><span class="ln"> 8</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></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="c">/* 給 pagefind cascade 的 scale */</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>一個 selector 看到所有 search 相關 token、cascade 到子樹生效。</p>
<h3 id="執行">執行</h3>
<p>JS 量測寫入 scope-h 時、寫到 <code>body.page-search</code> 而非 <code>:root</code>：</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="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">h</span> <span class="o">=</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">||</span> <span class="mi">56</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</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">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>寫到 body.style 直接覆蓋 body.page-search 的 fallback 值。Cascade 到所有後代生效。</p>
<hr>
<h2 id="變數命名與分類">變數命名與分類</h2>
<h3 id="命名前綴標明範圍">命名前綴標明範圍</h3>
<table>
  <thead>
      <tr>
          <th>前綴</th>
          <th>範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--token-*</code> 或無前綴</td>
          <td>全站設計 token（顏色、字型）</td>
      </tr>
      <tr>
          <td><code>--page-search-*</code></td>
          <td>搜尋頁專用</td>
      </tr>
      <tr>
          <td><code>--pagefind-ui-*</code></td>
          <td>Pagefind 提供的 hook（不是我們命名、是組件預期）</td>
      </tr>
  </tbody>
</table>
<p>前綴讓維護者一眼看出變數的「歸屬」、不會誤改別處變數。</p>
<h3 id="分類定義">分類定義</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">/* === 對齊 token === */</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="nv">--search-gap</span><span class="p">:</span> <span class="mi">20</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="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">/* JS 寫入 */</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">/* === 響應式 breakpoint === */</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c">/* (CSS 變數無法用在 @media query、breakpoint 寫死在 query 內) */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="c">/* === 對組件的 hook === */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>分類註解讓維護者知道「我要改哪類 token」、找對位置。</p>
<hr>
<h2 id="內在屬性比較四種變數定義方式">內在屬性比較：四種變數定義方式</h2>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>維護成本</th>
          <th>可見性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>散在多個 selector 定義</td>
          <td>高 — grep 找定義</td>
          <td>低 — 不知哪個生效</td>
      </tr>
      <tr>
          <td>集中在一個 selector</td>
          <td>低 — 改一處</td>
          <td>高 — 全部變數一覽</td>
      </tr>
      <tr>
          <td>集中 + 分類註解</td>
          <td>低</td>
          <td>最高 — 結構化</td>
      </tr>
      <tr>
          <td>集中 + JS 寫入用同一 selector</td>
          <td>低</td>
          <td>最高 + JS 動態同步</td>
      </tr>
  </tbody>
</table>
<p>優先選「集中 + 分類 + JS 寫入同 selector」。</p>
<hr>
<h2 id="變數的-fallback-策略">變數的 fallback 策略</h2>
<blockquote>
<p><strong>責任邊界</strong>：本節只談「fallback 值寫在哪個 selector」、屬於定義位置議題。「該不該用 runtime 量測」這個更上層的策略選擇由 <a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a> 處理 — 那邊主張「全寫死 vs 全量測、不要混搭」。</p></blockquote>
<p>JS 量測寫入的變數、CSS 應該有 fallback 值供 JS 還沒跑完時用：</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="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、JS 會覆寫 */</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="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">6</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">scope</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 class="c">/* JS 跑完前用 60px */</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>或用 <code>var()</code> 第二參數：</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="nt">margin-top</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--search-scope-h</span><span class="o">,</span> <span class="nt">60px</span><span class="o">)</span> <span class="o">+</span> <span class="nt">8px</span><span class="o">);</span></span></span></code></pre></div><p>兩種寫法效果相近 — 第一種讓 token 集中在 body.page-search 內、推薦使用。</p>
<hr>
<h2 id="設計取捨css-變數定義位置策略">設計取捨：CSS 變數定義位置策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（集中在使用範圍的最高層）當預設、其他做法在特定情境合理。</p>
<blockquote>
<p>本篇是 <a href="../single-source-of-truth/">#44 SSoT</a> 抽象原則在「CSS 變數定義位置」這個面向的應用。</p></blockquote>
<h3 id="a集中在跟使用範圍最匹配的最高層selector這個專案的預設">A：集中在「跟使用範圍最匹配的最高層」selector（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：全站 token 在 <code>:root</code>、頁面 token 在 <code>body.page-X</code>、組件 token 在 <code>.component</code>、JS 寫入也用同 selector</li>
<li><strong>選 A 的理由</strong>：定義住址唯一、改 token 自動跟上、cascade 範圍跟使用範圍一致</li>
<li><strong>適合</strong>：絕大多數 design token 系統</li>
<li><strong>代價</strong>：要先想清楚每個變數的「使用範圍」、不能無腦丟一處</li>
</ul>
<h3 id="b所有變數都丟-root">B：所有變數都丟 <code>:root</code></h3>
<ul>
<li><strong>機制</strong>：不分使用範圍、全部 <code>:root</code></li>
<li><strong>跟 A 的取捨</strong>：B 簡單一致、A 按範圍分；但 B 不在乎 scope、可能跟其他組件變數命名衝突、且 cascade 範圍過大</li>
<li><strong>B 比 A 好的情境</strong>：純 design system token（顏色 / 字型）、確實全站適用</li>
</ul>
<h3 id="c散在多個-selector-各自定義">C：散在多個 selector 各自定義</h3>
<ul>
<li><strong>機制</strong>：每個 component 各自定義需要的變數</li>
<li><strong>跟 A 的取捨</strong>：C 元件自包含、A 集中管理；但 C 同名 token 散落多處、cascade 順序決定值、改一處可能漏其他</li>
<li><strong>C 才合理的情境</strong>：完全獨立的元件、不共用任何 token（罕見）</li>
</ul>
<h3 id="d每處引用點都重複定義">D：每處引用點都重複定義</h3>
<ul>
<li><strong>機制</strong>：用 var 引用前都重新宣告一次</li>
<li><strong>D 是反模式</strong>：徹底違反 SSoT、改 token 要 grep 找全、必漏改 — 重複定義是 magic number 散落的另一種形式</li>
<li><strong>看起來吸引人的原因</strong>：每處就地寫值最快、不用想 token 該定義在哪</li>
<li><strong>實際發生的代價</strong>：未來改值時掃不到全部、UI 出現「有的地方變、有的沒變」的怪 bug</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同名變數在多個 selector 定義</td>
          <td>集中到一個 selector、移除其他</td>
      </tr>
      <tr>
          <td>改一個 token 要 grep 找定義位置</td>
          <td>集中 + 分類註解</td>
      </tr>
      <tr>
          <td>Token 命名沒前綴、跟其他組件變數混</td>
          <td>加範圍前綴（<code>--page-X-*</code>）</td>
      </tr>
      <tr>
          <td>JS 寫入變數的位置跟 CSS 定義不同</td>
          <td>對齊到同一 selector</td>
      </tr>
      <tr>
          <td>變數值在 cascade 中被另一處覆蓋</td>
          <td>找出兩處、決定哪一處保留</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：CSS 變數是設計 token 系統的基礎、定義位置就是 token 的「住址」。住址一個就好、不要一物多址。</p>
<p>「就地寫個值」是便利（不用找 token 位置）、「集中定義 + 引用」是對齊 — 同 <a href="../single-source-of-truth/">#44 SSOT</a> 跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 便利 vs 對齊反相關</a>。</p>
]]></content:encoded></item><item><title>runtime 量測模式統一</title><link>https://tarrragon.github.io/blog/report/runtime-measurement-unification/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/runtime-measurement-unification/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>對齊基準上的尺寸值、要嘛統一寫死、要嘛統一 runtime 量測 — 不要混搭。&lt;/strong> 混搭時某些變化（字型替換、scale 改變、theme 切換）會打破對齊、且問題只在特定情境出現、難以重現。選一邊走到底。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼混搭會不穩">為什麼混搭會不穩&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>對齊問題本質是「方程組」 — 每個變數的值都要正確、結果才對。&lt;/p>
&lt;p>寫死值的特徵：&lt;/p>
&lt;ul>
&lt;li>來源是 build time 設計決定&lt;/li>
&lt;li>變動需要手動改 CSS&lt;/li>
&lt;li>假設某個渲染條件成立（特定字型、特定 scale）&lt;/li>
&lt;/ul>
&lt;p>量測值的特徵：&lt;/p>
&lt;ul>
&lt;li>來源是 runtime DOM 量測&lt;/li>
&lt;li>自動跟著實際渲染走&lt;/li>
&lt;li>不依賴特定渲染條件&lt;/li>
&lt;/ul>
&lt;p>混搭時的失敗模式：寫死值依賴的渲染條件變了、但量測值跟著變、寫死值沒跟 — 兩者錯位、對齊壞掉。&lt;/p>
&lt;h3 id="統一往一邊靠的選擇">統一往一邊靠的選擇&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>統一策略&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全部寫死（鎖渲染條件）&lt;/td>
 &lt;td>設計 token 穩定、組件提供 scale hook 可鎖定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>全部量測（runtime 同步）&lt;/td>
 &lt;td>內容動態、字型 / 排版可能變動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇看「願意接受多少不確定性」 — 全寫死要鎖很多條件、全量測要寫多個 ResizeObserver。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>--search-title-h&lt;/code> (H1)&lt;/td>
 &lt;td>寫死 64px&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--search-form-h&lt;/code> (input)&lt;/td>
 &lt;td>寫死 68px、靠 &lt;code>--pagefind-ui-scale: 1.0&lt;/code> 鎖定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--search-gap&lt;/code> (drawer 上方)&lt;/td>
 &lt;td>寫死 20px&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--search-scope-h&lt;/code>&lt;/td>
 &lt;td>ResizeObserver 量測寫回&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>混搭：前三個寫死、第四個量測。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>當前情境穩定 — pagefind scale 鎖在 1.0、theme h1 高度可預測。但若：&lt;/p>
&lt;ul>
&lt;li>Theme 升級改 h1 line-height → 寫死 64px 不準&lt;/li>
&lt;li>使用者裝置字型不同 → form 內容寬度變動可能間接影響高度&lt;/li>
&lt;li>pagefind 升級 input 高度算法 → 寫死 68px 不準&lt;/li>
&lt;/ul>
&lt;p>寫死值「假設某些條件成立」、條件變了寫死值就錯。&lt;/p>
&lt;h3 id="執行兩種統一方向">執行：兩種統一方向&lt;/h3>
&lt;h4 id="方向-1全部寫死鎖更多渲染條件">方向 1：全部寫死、鎖更多渲染條件&lt;/h4>





&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="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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nv">--search-scope-h&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">56&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 不再 JS 量測 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nv">--pagefind-ui-scale&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-title&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">height&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>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">line-height&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>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">margin&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 鎖 H1 margin */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">.&lt;/span>&lt;span class="nc">search-scope&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="k">height&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">scope&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">h&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c">/* 鎖 scope 高度、超過裁掉 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">overflow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">hidden&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>代價：scope 內容超過時被裁、UI 可能不適合動態內容。&lt;/p>
&lt;h4 id="方向-2全部量測resizeobserver-同步所有">方向 2：全部量測、ResizeObserver 同步所有&lt;/h4>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">measureAll&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-title-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">titleEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-form-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">formEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// gap 是 pagefind drawer 內建、無法從外部量測
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setVar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">val&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">val&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&amp;#39;&lt;/span>&lt;span class="p">);&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">titleEl&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">formEl&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">measureAll&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>代價：JS 多了一層、初始載入時 fallback 值不對齊（直到 JS 跑完）。&lt;/p>
&lt;h3 id="推薦">推薦&lt;/h3>
&lt;p>&lt;strong>這個專案選方向 1（全寫死）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Pagefind scale 已能鎖定&lt;/li>
&lt;li>Theme 由本人控制、h1 變動可預期&lt;/li>
&lt;li>Scope UI 設計成單行、不需要動態高度&lt;/li>
&lt;/ul>
&lt;p>把當前 scope-h 從量測改寫死、移除 ResizeObserver。混搭問題消失。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>對齊基準上的尺寸值、要嘛統一寫死、要嘛統一 runtime 量測 — 不要混搭。</strong> 混搭時某些變化（字型替換、scale 改變、theme 切換）會打破對齊、且問題只在特定情境出現、難以重現。選一邊走到底。</p>
<hr>
<h2 id="為什麼混搭會不穩">為什麼混搭會不穩</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>對齊問題本質是「方程組」 — 每個變數的值都要正確、結果才對。</p>
<p>寫死值的特徵：</p>
<ul>
<li>來源是 build time 設計決定</li>
<li>變動需要手動改 CSS</li>
<li>假設某個渲染條件成立（特定字型、特定 scale）</li>
</ul>
<p>量測值的特徵：</p>
<ul>
<li>來源是 runtime DOM 量測</li>
<li>自動跟著實際渲染走</li>
<li>不依賴特定渲染條件</li>
</ul>
<p>混搭時的失敗模式：寫死值依賴的渲染條件變了、但量測值跟著變、寫死值沒跟 — 兩者錯位、對齊壞掉。</p>
<h3 id="統一往一邊靠的選擇">統一往一邊靠的選擇</h3>
<table>
  <thead>
      <tr>
          <th>統一策略</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全部寫死（鎖渲染條件）</td>
          <td>設計 token 穩定、組件提供 scale hook 可鎖定</td>
      </tr>
      <tr>
          <td>全部量測（runtime 同步）</td>
          <td>內容動態、字型 / 排版可能變動</td>
      </tr>
  </tbody>
</table>
<p>選擇看「願意接受多少不確定性」 — 全寫死要鎖很多條件、全量測要寫多個 ResizeObserver。</p>
<hr>
<h2 id="這次任務的混搭問題">這次任務的混搭問題</h2>
<h3 id="觀察">觀察</h3>
<p>對齊基準上四個值的處理：</p>
<table>
  <thead>
      <tr>
          <th>值</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--search-title-h</code> (H1)</td>
          <td>寫死 64px</td>
      </tr>
      <tr>
          <td><code>--search-form-h</code> (input)</td>
          <td>寫死 68px、靠 <code>--pagefind-ui-scale: 1.0</code> 鎖定</td>
      </tr>
      <tr>
          <td><code>--search-gap</code> (drawer 上方)</td>
          <td>寫死 20px</td>
      </tr>
      <tr>
          <td><code>--search-scope-h</code></td>
          <td>ResizeObserver 量測寫回</td>
      </tr>
  </tbody>
</table>
<p>混搭：前三個寫死、第四個量測。</p>
<h3 id="判讀">判讀</h3>
<p>當前情境穩定 — pagefind scale 鎖在 1.0、theme h1 高度可預測。但若：</p>
<ul>
<li>Theme 升級改 h1 line-height → 寫死 64px 不準</li>
<li>使用者裝置字型不同 → form 內容寬度變動可能間接影響高度</li>
<li>pagefind 升級 input 高度算法 → 寫死 68px 不準</li>
</ul>
<p>寫死值「假設某些條件成立」、條件變了寫死值就錯。</p>
<h3 id="執行兩種統一方向">執行：兩種統一方向</h3>
<h4 id="方向-1全部寫死鎖更多渲染條件">方向 1：全部寫死、鎖更多渲染條件</h4>





<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-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"> 3</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"> 4</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></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nv">--search-scope-h</span><span class="p">:</span> <span class="mi">56</span><span class="kt">px</span><span class="p">;</span>            <span class="c">/* 不再 JS 量測 */</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</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></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">.</span><span class="nc">search-title</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">height</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></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">line-height</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></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>                         <span class="c">/* 鎖 H1 margin */</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">.</span><span class="nc">search-scope</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="k">height</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">scope</span><span class="o">-</span><span class="n">h</span><span class="p">);</span>     <span class="c">/* 鎖 scope 高度、超過裁掉 */</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="k">overflow</span><span class="p">:</span> <span class="kc">hidden</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>代價：scope 內容超過時被裁、UI 可能不適合動態內容。</p>
<h4 id="方向-2全部量測resizeobserver-同步所有">方向 2：全部量測、ResizeObserver 同步所有</h4>





<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">measureAll</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">setVar</span><span class="p">(</span><span class="s1">&#39;--search-title-h&#39;</span><span class="p">,</span> <span class="nx">titleEl</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">setVar</span><span class="p">(</span><span class="s1">&#39;--search-form-h&#39;</span><span class="p">,</span> <span class="nx">formEl</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">setVar</span><span class="p">(</span><span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// gap 是 pagefind drawer 內建、無法從外部量測
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">function</span> <span class="nx">setVar</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">val</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</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="nx">name</span><span class="p">,</span> <span class="nx">val</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">[</span><span class="nx">titleEl</span><span class="p">,</span> <span class="nx">formEl</span><span class="p">,</span> <span class="nx">scopeEl</span><span class="p">].</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">measureAll</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>代價：JS 多了一層、初始載入時 fallback 值不對齊（直到 JS 跑完）。</p>
<h3 id="推薦">推薦</h3>
<p><strong>這個專案選方向 1（全寫死）</strong>：</p>
<ul>
<li>Pagefind scale 已能鎖定</li>
<li>Theme 由本人控制、h1 變動可預期</li>
<li>Scope UI 設計成單行、不需要動態高度</li>
</ul>
<p>把當前 scope-h 從量測改寫死、移除 ResizeObserver。混搭問題消失。</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>低 — 純 CSS</td>
          <td>低 — 動態內容超過值會裁</td>
      </tr>
      <tr>
          <td>全量測 ResizeObserver</td>
          <td>高 — 值跟著實際走</td>
          <td>中 — JS 多一層</td>
          <td>高</td>
      </tr>
      <tr>
          <td>混搭（部分寫死、部分量測）</td>
          <td>中 — 邊界 case 壞</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Magic number 估算</td>
          <td>低</td>
          <td>不適用</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>內容靜態 → 全寫死；內容動態 → 全量測；不要混搭</strong>。</p>
<hr>
<h2 id="鎖定渲染條件的具體技巧">鎖定渲染條件的具體技巧</h2>
<h3 id="1-使用組件提供的-scale-hook">1. 使用組件提供的 scale hook</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="p">.</span><span class="nc">search-shell</span> <span class="p">{</span> <span class="nv">--pagefind-ui-scale</span><span class="p">:</span> <span class="mf">1.0</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>讓組件按我們指定的 scale 渲染、寫死值才有意義。</p>
<h3 id="2-寫死-h1-height--line-height--margin">2. 寫死 H1 height + line-height + margin</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="p">.</span><span class="nc">search-title</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">height</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">3</span><span class="cl">  <span class="k">line-height</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="k">margin</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c">/* 確保 box height 永遠是 64、不受 font / padding 影響 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不留任何「看 box-sizing 與 inheritance 決定」的空間。</p>
<h3 id="3-用-box-sizing-border-box-確保-padding-不影響-box-height">3. 用 box-sizing: border-box 確保 padding 不影響 box height</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="p">.</span><span class="nc">search-scope</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">box-sizing</span><span class="p">:</span> <span class="kc">border-box</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">height</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">scope</span><span class="o">-</span><span class="n">h</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span> <span class="mi">16</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="c">/* total height 還是 var(--search-scope-h)、padding 算在內 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="設計取捨對齊基準上來源機制的選擇">設計取捨：對齊基準上來源機制的選擇</h2>
<p>四種做法、各自機會成本不同。預設依內容性質選 — 內容靜態 → A、內容動態 → B、混搭 / 估算永遠不是答案。</p>
<blockquote>
<p>本篇是 <a href="../single-source-of-truth/">#44 SSoT</a> 抽象原則在「來源機制統一」這個面向的應用。</p></blockquote>
<h3 id="a全寫死--鎖渲染條件內容靜態的預設">A：全寫死 + 鎖渲染條件（內容靜態的預設）</h3>
<ul>
<li><strong>機制</strong>：所有對齊基準值用 CSS 變數寫死、同時鎖定相關渲染條件（pagefind scale、H1 line-height、box-sizing）</li>
<li><strong>選 A 的理由</strong>：純 CSS 不依 JS、值 build time 確定、改 token 自動跟上</li>
<li><strong>適合</strong>：對齊內容靜態、可預測（設計穩定的搜尋頁、文章頁）</li>
<li><strong>代價</strong>：需要鎖很多渲染條件（scale / line-height / box-sizing 等）、scope 內容超過寫死值會被裁</li>
</ul>
<h3 id="b全量測-resizeobserver-寫回變數內容動態的預設">B：全量測 ResizeObserver 寫回變數（內容動態的預設）</h3>
<ul>
<li><strong>機制</strong>：所有對齊基準值用 ResizeObserver 量、寫回 CSS 變數、其他元素引用</li>
<li><strong>跟 A 的取捨</strong>：B 自動跟著實際渲染、A 假設條件穩定；B 多 JS 一層、初始 fallback 值不對齊（直到 JS 跑完）</li>
<li><strong>B 比 A 好的情境</strong>：內容動態（字型可能變、theme 切換、跨環境部署）</li>
</ul>
<h3 id="c混搭部分寫死部分量測">C：混搭（部分寫死、部分量測）</h3>
<ul>
<li><strong>機制</strong>：「主要值寫死、邊界值量測」混合策略</li>
<li><strong>C 是反模式</strong>：邊界情境（字型變、scale 變、theme 切換）下兩者錯位、對齊在某些 case 壞、難以重現</li>
<li><strong>看起來吸引人的原因</strong>：「主要情境寫死、邊界情境量測」聽起來合理、實際「主要 vs 邊界」判斷不可靠</li>
<li><strong>實際發生的代價</strong>：邊界常常變主要、混搭策略下 debug 範圍從「某情境」擴張到「整套是不是錯」</li>
</ul>
<h3 id="dmagic-number-估算">D：Magic number 估算</h3>
<ul>
<li><strong>機制</strong>：執行者依感覺給數字、不寫變數、不量測</li>
<li><strong>D 是反模式</strong>：任何「沒來源」的值都會在邊界情境爆掉 — 跨情境（字型 / scale / theme）必壞</li>
<li><strong>看起來吸引人的原因</strong>：執行者依感覺給數字最快、不用 query 也不用 query DevTools</li>
<li><strong>實際發生的代價</strong>：估錯時錯誤被視覺接受、ship 後在邊界情境暴露、信任損失</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對齊在某些字型 / 主題 / 縮放下壞掉</td>
          <td>找出依賴的渲染條件、鎖定或改量測</td>
      </tr>
      <tr>
          <td>改了某個 token 要去多處驗證對齊</td>
          <td>統一來源（全寫死 or 全量測）</td>
      </tr>
      <tr>
          <td>ResizeObserver 量了 A、B 卻寫死</td>
          <td>評估 B 是否也需要量、避免混搭</td>
      </tr>
      <tr>
          <td>寫死值跟實際渲染差距 &gt; 2px</td>
          <td>該值依賴的條件沒鎖、改量測或鎖條件</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：對齊問題的根因常常是「混搭」 — 用統一策略消除這個根因、debug 範圍從「某個情境」縮到「整套策略對嗎」。</p>
<p>混搭通常是便利驅動的結果（每處用最快的方式）、統一策略需要先對齊原則 — 同 <a href="../single-source-of-truth/">#44 SSOT</a> 跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 便利 vs 對齊反相關</a>。</p>
]]></content:encoded></item><item><title>以 class toggle 取代 inline `display: none !important`</title><link>https://tarrragon.github.io/blog/report/class-toggle-over-important/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/class-toggle-over-important/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>JS 改 DOM 元素的視覺狀態、用 class toggle、不用 inline style.setProperty important hack。&lt;/strong> Class toggle 的好處：CSS 規則集中可讀、devtools 看到語意化的 class 名而非神秘 inline style、未來改視覺只動 CSS 不動 JS。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-class-toggle-比-inline-style-好">為什麼 class toggle 比 inline style 好&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>兩種方式都能達成「JS 控制視覺」、差別在「視覺規則的家在哪」：&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;code>el.style.display = 'none'&lt;/code>&lt;/td>
 &lt;td>散在 JS 各處&lt;/td>
 &lt;td>高 — 改視覺要找 JS、不在 CSS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>el.classList.toggle('is-hidden')&lt;/code> + &lt;code>.is-hidden { display: none }&lt;/code>&lt;/td>
 &lt;td>集中在 CSS&lt;/td>
 &lt;td>低 — 改視覺改 CSS&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>集中在 CSS = 設計系統的單一來源、devtools Element 面板看 class 知道狀態、code review 容易理解。&lt;/p>
&lt;p>&lt;code>!important&lt;/code> 的需求消失：只要 CSS Layers 把 vendor CSS 包進低權層、自家 unlayered CSS 自然贏、不需要 important。&lt;/p>
&lt;h3 id="何時-inline-style-是必要的">何時 inline style 是必要的&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>inline style 必要&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>動態值（隨 runtime 計算）&lt;/td>
 &lt;td>是 — 如 &lt;code>el.style.top = scrollY + 'px'&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>動畫起點 / 終點&lt;/td>
 &lt;td>是 — &lt;code>el.style.transform = ...&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>切換 boolean 狀態（顯示/隱藏）&lt;/td>
 &lt;td>否 — 用 class toggle&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>套用設計系統一致樣式&lt;/td>
 &lt;td>否 — 用 class toggle&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「狀態切換」是 class toggle 的場景、不是 inline style 的場景。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的重構機會">這次任務的重構機會&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>Scope filter 用：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">show&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">title&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">excerpt&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">show&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="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">removeProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;display&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;display&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;none&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;important&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>setProperty important&lt;/code> 是為了壓過 Svelte 重繪可能 reset 的 inline style。CSS Layers 之後、important 不再必要。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>改用 class toggle + layered CSS：&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">/* assets/search.css，unlayered（pagefind 在 layer(pagefind) 內） */&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="nc">pagefind-ui__result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">is-scope-filtered&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="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">show&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">title&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">excerpt&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toggle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;is-scope-filtered&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">show&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>更乾淨：&lt;/p>
&lt;ul>
&lt;li>CSS 規則集中&lt;/li>
&lt;li>DevTools Element 面板看到 &lt;code>.is-scope-filtered&lt;/code> 就知道為什麼隱藏&lt;/li>
&lt;li>JS 邏輯簡化（&lt;code>classList.toggle&lt;/code> 一行解兩種狀態）&lt;/li>
&lt;li>不需要 &lt;code>!important&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="執行-prerequisite">執行 prerequisite&lt;/h3>
&lt;p>要這個 refactor 生效、先做 #24（CSS Layers）：&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="p">@&lt;/span>&lt;span class="k">import&lt;/span> &lt;span class="nt">url&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="s2">&amp;#34;/blog/pagefind/pagefind-ui.css&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="nt">layer&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nt">pagefind&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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 class="c">/* unlayered，自動勝過 pagefind 的所有 specificity */&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">pagefind-ui__result&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nc">is-scope-filtered&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">none&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>否則 layered 的 pagefind CSS 可能用 specificity 30 蓋過 &lt;code>.is-scope-filtered&lt;/code>（specificity 20）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>JS 改 DOM 元素的視覺狀態、用 class toggle、不用 inline style.setProperty important hack。</strong> Class toggle 的好處：CSS 規則集中可讀、devtools 看到語意化的 class 名而非神秘 inline style、未來改視覺只動 CSS 不動 JS。</p>
<hr>
<h2 id="為什麼-class-toggle-比-inline-style-好">為什麼 class toggle 比 inline style 好</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>兩種方式都能達成「JS 控制視覺」、差別在「視覺規則的家在哪」：</p>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>視覺規則住址</th>
          <th>維護成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>el.style.display = 'none'</code></td>
          <td>散在 JS 各處</td>
          <td>高 — 改視覺要找 JS、不在 CSS</td>
      </tr>
      <tr>
          <td><code>el.classList.toggle('is-hidden')</code> + <code>.is-hidden { display: none }</code></td>
          <td>集中在 CSS</td>
          <td>低 — 改視覺改 CSS</td>
      </tr>
  </tbody>
</table>
<p>集中在 CSS = 設計系統的單一來源、devtools Element 面板看 class 知道狀態、code review 容易理解。</p>
<p><code>!important</code> 的需求消失：只要 CSS Layers 把 vendor CSS 包進低權層、自家 unlayered CSS 自然贏、不需要 important。</p>
<h3 id="何時-inline-style-是必要的">何時 inline style 是必要的</h3>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>inline style 必要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>動態值（隨 runtime 計算）</td>
          <td>是 — 如 <code>el.style.top = scrollY + 'px'</code></td>
      </tr>
      <tr>
          <td>動畫起點 / 終點</td>
          <td>是 — <code>el.style.transform = ...</code></td>
      </tr>
      <tr>
          <td>切換 boolean 狀態（顯示/隱藏）</td>
          <td>否 — 用 class toggle</td>
      </tr>
      <tr>
          <td>套用設計系統一致樣式</td>
          <td>否 — 用 class toggle</td>
      </tr>
  </tbody>
</table>
<p>「狀態切換」是 class toggle 的場景、不是 inline style 的場景。</p>
<hr>
<h2 id="這次任務的重構機會">這次任務的重構機會</h2>
<h3 id="觀察">觀察</h3>
<p>Scope filter 用：</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="nx">items</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">show</span> <span class="o">=</span> <span class="nx">scope</span> <span class="o">===</span> <span class="s1">&#39;title&#39;</span> <span class="o">?</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="o">:</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">excerpt</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">show</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">removeProperty</span><span class="p">(</span><span class="s1">&#39;display&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">el</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;display&#39;</span><span class="p">,</span> <span class="s1">&#39;none&#39;</span><span class="p">,</span> <span class="s1">&#39;important&#39;</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></code></pre></div><p><code>setProperty important</code> 是為了壓過 Svelte 重繪可能 reset 的 inline style。CSS Layers 之後、important 不再必要。</p>
<h3 id="判讀">判讀</h3>
<p>改用 class toggle + layered CSS：</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">/* assets/search.css，unlayered（pagefind 在 layer(pagefind) 內） */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__result</span><span class="p">.</span><span class="nc">is-scope-filtered</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<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="nx">items</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">show</span> <span class="o">=</span> <span class="nx">scope</span> <span class="o">===</span> <span class="s1">&#39;title&#39;</span> <span class="o">?</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="o">:</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">excerpt</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s1">&#39;is-scope-filtered&#39;</span><span class="p">,</span> <span class="o">!</span><span class="nx">show</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>更乾淨：</p>
<ul>
<li>CSS 規則集中</li>
<li>DevTools Element 面板看到 <code>.is-scope-filtered</code> 就知道為什麼隱藏</li>
<li>JS 邏輯簡化（<code>classList.toggle</code> 一行解兩種狀態）</li>
<li>不需要 <code>!important</code></li>
</ul>
<h3 id="執行-prerequisite">執行 prerequisite</h3>
<p>要這個 refactor 生效、先做 #24（CSS Layers）：</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="p">@</span><span class="k">import</span> <span class="nt">url</span><span class="o">(</span><span class="s2">&#34;/blog/pagefind/pagefind-ui.css&#34;</span><span class="o">)</span> <span class="nt">layer</span><span class="o">(</span><span class="nt">pagefind</span><span class="o">)</span><span class="p">;</span>
</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="c">/* unlayered，自動勝過 pagefind 的所有 specificity */</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__result</span><span class="p">.</span><span class="nc">is-scope-filtered</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>否則 layered 的 pagefind CSS 可能用 specificity 30 蓋過 <code>.is-scope-filtered</code>（specificity 20）。</p>
<hr>
<h2 id="內在屬性比較四種-js-控制視覺方式">內在屬性比較：四種 JS 控制視覺方式</h2>
<table>
  <thead>
      <tr>
          <th>方式</th>
          <th>維護成本</th>
          <th>DevTools 可讀性</th>
          <th>Important 需求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>el.style.display = 'none'</code></td>
          <td>中 — 規則散在 JS</td>
          <td>中 — 看到 inline style</td>
          <td>否</td>
      </tr>
      <tr>
          <td><code>el.style.setProperty('display','none','important')</code></td>
          <td>高 — important 散在 JS</td>
          <td>中</td>
          <td>是 — 跟 framework 競爭</td>
      </tr>
      <tr>
          <td><code>el.classList.toggle('is-hidden')</code> + CSS</td>
          <td>低 — 規則在 CSS</td>
          <td>高 — 看 class 知狀態</td>
          <td>否（CSS Layers 環境下）</td>
      </tr>
      <tr>
          <td><code>el.dataset.state = 'hidden'</code> + <code>[data-state=hidden]</code> CSS</td>
          <td>低 — 規則在 CSS</td>
          <td>高 — attribute 也表達狀態</td>
          <td>否</td>
      </tr>
  </tbody>
</table>
<p>優先選 class toggle（或 dataset） — CSS-first、可讀、可維護。</p>
<hr>
<h2 id="class-toggle-的命名慣例">Class toggle 的命名慣例</h2>
<h3 id="用-is-x--has-x-表狀態">用 <code>is-X</code> / <code>has-X</code> 表狀態</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="p">.</span><span class="nc">is-scope-filtered</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">is-loading</span> <span class="p">{</span> <span class="k">opacity</span><span class="p">:</span> <span class="mf">0.5</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">has-error</span> <span class="p">{</span> <span class="k">border-color</span><span class="p">:</span> <span class="kc">red</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p><code>is-</code> / <code>has-</code> 前綴讓「狀態 class」跟「結構 class」（如 <code>.search-shell</code>）視覺區分、code review 一眼看出哪些是動態狀態。</p>
<h3 id="用-bem-modifier">用 BEM modifier</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="p">.</span><span class="nc">search-result--filtered</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>BEM 風格也可以、看專案 convention。重點是有規律、不要混雜。</p>
<hr>
<h2 id="devtools-可讀性的具體差異">DevTools 可讀性的具體差異</h2>
<h3 id="inline-style-視角">Inline style 視角</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;pagefind-ui__result svelte-j9e30&#34;</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;display: none !important;&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>DevTools 顯示「inline style 設了 important」 — 但不知道為什麼。要 grep JS 找出哪段邏輯設的。</p>
<h3 id="class-toggle-視角">Class toggle 視角</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;pagefind-ui__result svelte-j9e30 is-scope-filtered&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>DevTools 顯示「有 <code>.is-scope-filtered</code> class」 — class 名本身解釋為什麼隱藏。CSS 面板也直接顯示對應規則。</p>
<hr>
<h2 id="設計取捨js-控制視覺狀態的策略">設計取捨：JS 控制視覺狀態的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（class toggle）當預設、其他做法在特定情境合理。</p>
<h3 id="aclass-toggle--css-規則這個專案的預設">A：Class toggle + CSS 規則（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>el.classList.toggle('is-scope-filtered')</code>、CSS 內定義 <code>.is-scope-filtered { display: none }</code></li>
<li><strong>選 A 的理由</strong>：CSS 規則集中、devtools 看 class 知狀態、改視覺只動 CSS、配 CSS Layers 不需 <code>!important</code></li>
<li><strong>適合</strong>：布林狀態切換（顯示 / 隱藏 / 啟用 / 停用）</li>
<li><strong>代價</strong>：需要在 CSS 預先定義 class 規則（多一份 CSS）</li>
</ul>
<h3 id="binline-stylex--">B：Inline <code>style.X = ...</code></h3>
<ul>
<li><strong>機制</strong>：<code>el.style.display = 'none'</code></li>
<li><strong>跟 A 的取捨</strong>：B 一行 JS、A 需要 CSS 規則；但 B 規則散在 JS 各處、devtools 看到 <code>display: none</code> inline 不知道為什麼</li>
<li><strong>B 比 A 好的情境</strong>：runtime 計算的動態值（<code>el.style.top = scrollY + 'px'</code>）— 這類值無法預先寫進 CSS</li>
</ul>
<h3 id="cinline--setpropertyimportant">C：Inline + <code>setProperty('important')</code></h3>
<ul>
<li><strong>機制</strong>：<code>el.style.setProperty('display', 'none', 'important')</code></li>
<li><strong>跟 A/B 的取捨</strong>：C 比 B 多 important、為了壓過 framework 重繪 reset 的 inline；但 C 進入 <code>!important</code> 戰、未來新 important 對撞 debug 困難</li>
<li><strong>C 才合理的情境</strong>：framework 強制 reset 自家 inline style、且不能用 layered CSS（極罕見）</li>
<li><strong>更好的解</strong>：用 <a href="../css-layers-over-specificity/">#24 CSS Layers</a> 解 specificity 戰、本卡片 A 即可</li>
</ul>
<h3 id="ddataset-attribute--css-attribute-selector">D：Dataset attribute + CSS attribute selector</h3>
<ul>
<li><strong>機制</strong>：<code>el.dataset.state = 'hidden'</code>、CSS <code>[data-state=&quot;hidden&quot;] { display: none }</code></li>
<li><strong>跟 A 的取捨</strong>：D 用 attribute 表狀態、A 用 class；D 在「狀態值多種」時更合適（例如 <code>data-state=&quot;loading|ready|error&quot;</code>）</li>
<li><strong>D 比 A 好的情境</strong>：狀態有多個值（不只 boolean）、需要 CSS attribute selector 表達多分支</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JS 用 <code>style.setProperty(..., 'important')</code></td>
          <td>改用 class toggle、用 CSS Layers 解決 specificity</td>
      </tr>
      <tr>
          <td><code>el.style.display = 'none'</code> 散落多處</td>
          <td>集中為 <code>.is-X</code> class、JS 只 toggle</td>
      </tr>
      <tr>
          <td>DevTools 看到 inline style 不知為什麼</td>
          <td>改用語意化 class、devtools 看 class 自帶解釋</td>
      </tr>
      <tr>
          <td>視覺改動要改 JS（不是 CSS）</td>
          <td>Refactor 為 class toggle、視覺改動只動 CSS</td>
      </tr>
      <tr>
          <td>改視覺需要對抗 framework reset</td>
          <td>用 CSS Layers 把 framework 規則降層、自家規則不需 important</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：CSS 是視覺規則的家、JS 控制狀態 - 兩者透過 class toggle 介面共處、不互相侵入。</p>
<p>Inline style + !important 是「立刻生效」的便利、class toggle 是「樣式留 CSS」的對齊 — 這個反相關的更高層原則見 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。</p>
]]></content:encoded></item><item><title>MutationObserver 範圍與觸發頻率：監聽最少必要的變動</title><link>https://tarrragon.github.io/blog/report/mutation-observer-scope/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mutation-observer-scope/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>MutationObserver 監聽最少必要的變動 — 從「監聽哪個 root」「觀察什麼類型」「多久觸發一次」三維度收斂。&lt;/strong> 範圍寬會頻繁觸發、option 勾多會在不關心的變動上跑邏輯、apply 自己改 DOM 會引發無限循環。三維度都該顯式設計、不能只丟預設。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-observer-需要獨立議題">為什麼 observer 需要獨立議題&lt;/h2>
&lt;h3 id="跟-selector-的差異">跟 selector 的差異&lt;/h3>
&lt;p>Observer 與 selector 都涉及「DOM 範圍」、機制完全不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Selector&lt;/th>
 &lt;th>Observer&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>隨 DOM 變動自動觸發&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>監聽範圍 + option + 頻率&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把 selector 與 observer 綁同一篇討論會混淆 — 兩者解決的是不同問題、有不同失敗模式、需要不同的設計工具。&lt;/p>
&lt;h3 id="observer-寬範圍的失敗模式">Observer 寬範圍的失敗模式&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗模式&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>根因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>過度觸發&lt;/td>
 &lt;td>短時間觸發數十次&lt;/td>
 &lt;td>subtree 太深 + option 太多&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在錯時機跑&lt;/td>
 &lt;td>layout 還沒穩就跑 apply&lt;/td>
 &lt;td>沒等 framework patch 結束&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>無限循環&lt;/td>
 &lt;td>apply 自己改 DOM 又觸發 observer&lt;/td>
 &lt;td>沒 disconnect/observe 保護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>漏掉變動&lt;/td>
 &lt;td>預期會觸發但沒觸發&lt;/td>
 &lt;td>option 沒勾對、或 root 選錯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四種都來自「沒精細設計 observer 的監聽形狀」。&lt;/p>
&lt;hr>
&lt;h2 id="三維度收斂">三維度收斂&lt;/h2>
&lt;h3 id="維度-1監聽哪個-root範圍">維度 1：監聽哪個 root（範圍）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：observer 的 root 元素決定「哪些範圍內的變動會被看到」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 寬：監聽整個 .pagefind-ui
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">apply&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 收斂：只監聽結果列表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__results&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">apply&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">results&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寬範圍把無關變動也帶進來 — pagefind 重繪 input、調整 filter、重排 chip 都會觸發 apply、但 apply 只關心結果變動。&lt;/p>
&lt;p>&lt;strong>Root 選擇的決策&lt;/strong>：找到「&lt;strong>包含所有目標變動、但不包含其他無關變動&lt;/strong>的最小元素」。&lt;/p>
&lt;ul>
&lt;li>太大 → 帶進無關變動、過度觸發&lt;/li>
&lt;li>太小 → 漏掉真正關心的變動&lt;/li>
&lt;li>剛好 → 只關心的變動觸發&lt;/li>
&lt;/ul>
&lt;p>問自己：「我關心的變動發生在哪些元素？這些元素的最小共同 ancestor 是誰？」答案就是 observer root。&lt;/p>
&lt;h3 id="維度-2觀察什麼類型option-flag">維度 2：觀察什麼類型（option flag）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：MutationObserver 提供四種 option、每種對應不同類型變動：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 子節點增 / 減 / 重排
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">attributes&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 屬性變動
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">attributeFilter&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;data-state&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="c1">// 只看特定屬性
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">characterData&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 文字內容變動
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 上面三種往子樹深處看
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">attributeOldValue&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 屬性變動時記錄舊值
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">characterDataOldValue&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設只勾需要的、不要全部 true：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Option&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>觸發頻率&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>childList: true&lt;/code>&lt;/td>
 &lt;td>子節點增減&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>childList + subtree&lt;/code>&lt;/td>
 &lt;td>任何深度的子節點增減&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>attributes&lt;/code> 全屬性&lt;/td>
 &lt;td>任何屬性變動&lt;/td>
 &lt;td>最高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>attributes + attributeFilter&lt;/code>&lt;/td>
 &lt;td>只特定屬性&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>characterData&lt;/code>&lt;/td>
 &lt;td>文字內容（少用）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>避免勾 subtree&lt;/strong>：subtree 把監聽從「直接子」擴展到「整個子樹」、觸發頻率可能爆炸。只在「真的需要看深層變動」時用。&lt;/p>
&lt;p>&lt;strong>避免無 filter 的 attributes&lt;/strong>：DOM 屬性變動很頻繁（class 改、style 改、aria-* 改），不過濾會被淹沒。用 &lt;code>attributeFilter: [...]&lt;/code> 縮到只看你關心的屬性。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>MutationObserver 監聽最少必要的變動 — 從「監聽哪個 root」「觀察什麼類型」「多久觸發一次」三維度收斂。</strong> 範圍寬會頻繁觸發、option 勾多會在不關心的變動上跑邏輯、apply 自己改 DOM 會引發無限循環。三維度都該顯式設計、不能只丟預設。</p>
<hr>
<h2 id="為什麼-observer-需要獨立議題">為什麼 observer 需要獨立議題</h2>
<h3 id="跟-selector-的差異">跟 selector 的差異</h3>
<p>Observer 與 selector 都涉及「DOM 範圍」、機制完全不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Selector</th>
          <th>Observer</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>時機</td>
          <td>同步、當下查詢</td>
          <td>非同步、回應未來變動</td>
      </tr>
      <tr>
          <td>執行頻率</td>
          <td>一次或顯式重呼叫</td>
          <td>隨 DOM 變動自動觸發</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>撈太多 / 撈太少</td>
          <td>觸發太頻繁 / 漏觸發 / 無限循環</td>
      </tr>
      <tr>
          <td>設計重點</td>
          <td>起點 + 範圍 + 過濾</td>
          <td>監聽範圍 + option + 頻率</td>
      </tr>
  </tbody>
</table>
<p>把 selector 與 observer 綁同一篇討論會混淆 — 兩者解決的是不同問題、有不同失敗模式、需要不同的設計工具。</p>
<h3 id="observer-寬範圍的失敗模式">Observer 寬範圍的失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>表現</th>
          <th>根因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>過度觸發</td>
          <td>短時間觸發數十次</td>
          <td>subtree 太深 + option 太多</td>
      </tr>
      <tr>
          <td>在錯時機跑</td>
          <td>layout 還沒穩就跑 apply</td>
          <td>沒等 framework patch 結束</td>
      </tr>
      <tr>
          <td>無限循環</td>
          <td>apply 自己改 DOM 又觸發 observer</td>
          <td>沒 disconnect/observe 保護</td>
      </tr>
      <tr>
          <td>漏掉變動</td>
          <td>預期會觸發但沒觸發</td>
          <td>option 沒勾對、或 root 選錯</td>
      </tr>
  </tbody>
</table>
<p>四種都來自「沒精細設計 observer 的監聽形狀」。</p>
<hr>
<h2 id="三維度收斂">三維度收斂</h2>
<h3 id="維度-1監聽哪個-root範圍">維度 1：監聽哪個 root（範圍）</h3>
<p><strong>核心定義</strong>：observer 的 root 元素決定「哪些範圍內的變動會被看到」。</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">// 寬：監聽整個 .pagefind-ui
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">ui</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</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="c1">// 收斂：只監聽結果列表
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__results&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>寬範圍把無關變動也帶進來 — pagefind 重繪 input、調整 filter、重排 chip 都會觸發 apply、但 apply 只關心結果變動。</p>
<p><strong>Root 選擇的決策</strong>：找到「<strong>包含所有目標變動、但不包含其他無關變動</strong>的最小元素」。</p>
<ul>
<li>太大 → 帶進無關變動、過度觸發</li>
<li>太小 → 漏掉真正關心的變動</li>
<li>剛好 → 只關心的變動觸發</li>
</ul>
<p>問自己：「我關心的變動發生在哪些元素？這些元素的最小共同 ancestor 是誰？」答案就是 observer root。</p>
<h3 id="維度-2觀察什麼類型option-flag">維度 2：觀察什麼類型（option flag）</h3>
<p><strong>核心定義</strong>：MutationObserver 提供四種 option、每種對應不同類型變動：</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="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</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="nx">attributes</span><span class="o">:</span> <span class="kc">true</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="nx">attributeFilter</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;data-state&#39;</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="nx">characterData</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>    <span class="c1">// 文字內容變動
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>          <span class="c1">// 上面三種往子樹深處看
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nx">attributeOldValue</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>  <span class="c1">// 屬性變動時記錄舊值
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>  <span class="nx">characterDataOldValue</span><span class="o">:</span> <span class="kc">true</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>預設只勾需要的、不要全部 true：</p>
<table>
  <thead>
      <tr>
          <th>Option</th>
          <th>用途</th>
          <th>觸發頻率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>childList: true</code></td>
          <td>子節點增減</td>
          <td>中</td>
      </tr>
      <tr>
          <td><code>childList + subtree</code></td>
          <td>任何深度的子節點增減</td>
          <td>高</td>
      </tr>
      <tr>
          <td><code>attributes</code> 全屬性</td>
          <td>任何屬性變動</td>
          <td>最高</td>
      </tr>
      <tr>
          <td><code>attributes + attributeFilter</code></td>
          <td>只特定屬性</td>
          <td>低</td>
      </tr>
      <tr>
          <td><code>characterData</code></td>
          <td>文字內容（少用）</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p><strong>避免勾 subtree</strong>：subtree 把監聽從「直接子」擴展到「整個子樹」、觸發頻率可能爆炸。只在「真的需要看深層變動」時用。</p>
<p><strong>避免無 filter 的 attributes</strong>：DOM 屬性變動很頻繁（class 改、style 改、aria-* 改），不過濾會被淹沒。用 <code>attributeFilter: [...]</code> 縮到只看你關心的屬性。</p>
<h3 id="維度-3多久觸發一次頻率">維度 3：多久觸發一次（頻率）</h3>
<p><strong>核心定義</strong>：observer 的回呼可能短時間內被連續呼叫、用 debounce 把多次合併成一次。</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="kd">var</span> <span class="nx">timer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">function</span> <span class="nx">schedule</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">timer</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(</span><span class="nx">apply</span><span class="p">,</span> <span class="mi">80</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="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">schedule</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>Debounce 80ms 表示「最後一次變動後 80ms 沒再變、才跑 apply」 — 把連續變動合併。</p>
<p><strong>Debounce vs Throttle</strong>：</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>行為</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Debounce</td>
          <td>安靜後執行</td>
          <td>等 framework 連續 patch 結束</td>
      </tr>
      <tr>
          <td>Throttle</td>
          <td>固定頻率執行</td>
          <td>UI 同步要立即反應、但限速</td>
      </tr>
      <tr>
          <td>立即執行</td>
          <td>每次都跑</td>
          <td>變動頻率本來就低、且每次都要處理</td>
      </tr>
  </tbody>
</table>
<p>大部分 observer 場景適合 debounce — framework patch 是突發性、不是持續的。</p>
<p><strong>Debounce 時間選擇</strong>：</p>
<table>
  <thead>
      <tr>
          <th>時間</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>16ms（一個 frame）</td>
          <td>跟 paint 同步、最即時</td>
      </tr>
      <tr>
          <td>50-100ms</td>
          <td>一般 UI 反應、肉眼感受不到延遲</td>
      </tr>
      <tr>
          <td>200-300ms</td>
          <td>等使用者輸入結束</td>
      </tr>
      <tr>
          <td>1000ms+</td>
          <td>後台處理、不影響 UI</td>
      </tr>
  </tbody>
</table>
<p>預設 50-100ms — 比一個 frame 寬、又不會讓使用者感受延遲。</p>
<hr>
<h2 id="self-mutation-循環的處理">Self-mutation 循環的處理</h2>
<h3 id="問題場景">問題場景</h3>
<p>apply 函式自己也改 DOM 時、會再次觸發 observer：</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="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 改了某個元素的 class（attribute 變動）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">someEl</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;processed&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">attributes</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</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="c1">// → apply 改 class 觸發 observer → observer 又呼叫 apply → 無限循環
</span></span></span></code></pre></div><p><strong>這不是邏輯錯、是 observer 機制的特性</strong>：observer 不會區分「是不是 apply 自己改的」。</p>
<h3 id="解法disconnect--observe-配對">解法：disconnect / observe 配對</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">var</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">observer</span><span class="p">.</span><span class="nx">disconnect</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="nx">apply</span><span class="p">();</span>                    <span class="c1">// 自己改 DOM 不會觸發
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</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="p">});</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span></span></span></code></pre></div><p>apply 期間 observer 暫停、apply 結束後恢復 — 自己的改動不會觸發自己。</p>
<h3 id="解法替代用-attribute-標記區分">解法替代：用 attribute 標記區分</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">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">isApplying</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">someEl</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;processed&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">isApplying</span> <span class="o">=</span> <span class="kc">false</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="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isApplying</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="nx">apply</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span></span></span></code></pre></div><p>但這個解法有時序風險 — observer 是非同步、<code>isApplying</code> 可能在錯時間被讀。<strong>disconnect/observe 配對更穩</strong>。</p>
<h3 id="解法替代root-與目標分離">解法替代：root 與目標分離</h3>
<p>如果 apply 改的是 A、observer 監聽的是 B（A 跟 B 沒交集），自然不循環：</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="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">resultsEl</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="c1">// 改的是 input 而不是 results — 不會觸發 observer
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">inputEl</span><span class="p">.</span><span class="nx">value</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="p">}</span></span></span></code></pre></div><p>設計時讓 observer 看的範圍跟 apply 改的範圍<strong>結構上分離</strong> — 是最乾淨的解法、不需要 disconnect 配對。</p>
<hr>
<h2 id="觀察的時機問題">觀察的時機問題</h2>
<h3 id="observer-跟-framework-渲染週期競爭">Observer 跟 framework 渲染週期競爭</h3>
<p>Observer 在 framework 連續 patch 中段觸發、可能在 layout 還沒穩時就跑 apply、造成短暫視覺錯位：</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">// framework 連續 patch：
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">//   patch 1 → observer 觸發 → apply 跑 → 視覺 A
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">//   patch 2 → observer 觸發 → apply 跑 → 視覺 B
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">//   patch 3 → observer 觸發 → apply 跑 → 視覺 C（最終）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 使用者看到 A → B → C 的閃爍
</span></span></span></code></pre></div><p>Debounce 是這個問題的解 — 讓 observer 等 patch 完成才跑 apply。</p>
<h3 id="確認時機正確">確認時機正確</h3>
<p>寫 observer 時自問：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>答案決定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Apply 跑的時候 layout 是否已穩定？</td>
          <td>是否需要 debounce</td>
      </tr>
      <tr>
          <td>Apply 自己改 DOM 嗎？</td>
          <td>是否需要 disconnect 配對</td>
      </tr>
      <tr>
          <td>我關心的變動類型是什麼？</td>
          <td>option flag 怎麼勾</td>
      </tr>
      <tr>
          <td>變動發生在哪一層？</td>
          <td>是否需要 subtree</td>
      </tr>
      <tr>
          <td>Framework 的渲染週期會干擾嗎？</td>
          <td>debounce 時間取多久</td>
      </tr>
  </tbody>
</table>
<p>每個問題都該有顯式答案、不能丟預設。</p>
<hr>
<h2 id="內在屬性比較四種-observer-設計">內在屬性比較：四種 observer 設計</h2>
<table>
  <thead>
      <tr>
          <th>設計</th>
          <th>觸發頻率</th>
          <th>Layout 穩定性</th>
          <th>維護成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全勾 + subtree + 無 debounce</td>
          <td>最高</td>
          <td>低 — patch 中段觸發</td>
          <td>低（短期）/ 高（debug 噩夢）</td>
      </tr>
      <tr>
          <td>收斂 root + 必要 option + 無 debounce</td>
          <td>中</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>收斂 root + 必要 option + debounce</td>
          <td>低</td>
          <td>高</td>
          <td>中</td>
      </tr>
      <tr>
          <td>結構分離 + 收斂 + debounce</td>
          <td>最低</td>
          <td>最高</td>
          <td>中（前期設計成本）</td>
      </tr>
  </tbody>
</table>
<p><strong>推薦</strong>：收斂 root + 必要 option + debounce。<code>apply</code> 不改 DOM 時不需要 disconnect；改的話用結構分離優先、退而求其次用 disconnect。</p>
<hr>
<h2 id="進階技巧">進階技巧</h2>
<h3 id="1-動態調整-observer-範圍">1. 動態調整 observer 範圍</h3>
<p>當監聽目標可能還沒 mount 時、用兩階段 observer：</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">// 階段 1：等目標 mount
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">bootstrap</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__results&#39;</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="o">!</span><span class="nx">target</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">bootstrap</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 階段 2：mount 後監聽目標
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">target</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">bootstrap</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>階段 1 用寬範圍找到目標、階段 2 切到精準範圍 — 把寬範圍的觸發限制在「找目標」這個短時間。</p>
<h3 id="2-用-takerecords-主動取出累積變動">2. 用 <code>takeRecords</code> 主動取出累積變動</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">var</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</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="c1">// 之後某時間點、想立刻處理累積的變動
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">records</span> <span class="o">=</span> <span class="nx">observer</span><span class="p">.</span><span class="nx">takeRecords</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">processRecords</span><span class="p">(</span><span class="nx">records</span><span class="p">);</span></span></span></code></pre></div><p><code>takeRecords</code> 取出尚未觸發回呼的變動記錄、主動處理 — 適合「我想在某時間點同步處理累積變動」場景。</p>
<h3 id="3-多-observer-各管一塊">3. 多 observer 各管一塊</h3>
<p>不要用一個 observer 監聽全部、各分一個：</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="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">applyA</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">elA</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">applyB</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">elB</span><span class="p">,</span> <span class="p">{</span> <span class="nx">attributes</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>各自獨立 — 一個 observer 出錯不影響另一個、debug 範圍小、option 各自最佳化。</p>
<hr>
<h2 id="設計取捨mutationobserver-的設計策略">設計取捨：MutationObserver 的設計策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（收斂 root + 必要 option + debounce）當預設、其他做法在特定情境合理。</p>
<h3 id="a收斂-root--必要-option--debounce--結構分離這個專案的預設">A：收斂 root + 必要 option + debounce + 結構分離（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：root 取最小共同 ancestor、option 只勾真正關心的變動、加 50-100ms debounce、apply 改的範圍跟 observer 看的範圍結構上分離</li>
<li><strong>選 A 的理由</strong>：觸發頻率最低、layout 穩定、無 self-mutation 循環風險</li>
<li><strong>適合</strong>：絕大多數 observer 設計</li>
<li><strong>代價</strong>：前期設計成本中（要思考 root / option / 結構）</li>
</ul>
<h3 id="b收斂-root--必要-option無-debounce">B：收斂 root + 必要 option（無 debounce）</h3>
<ul>
<li><strong>機制</strong>：縮範圍與 option、但不加 debounce</li>
<li><strong>跟 A 的取捨</strong>：B 即時反應、A 等 debounce；但 B 在 framework patch 中段觸發、layout 不穩時跑 apply 結果不可靠</li>
<li><strong>B 比 A 好的情境</strong>：apply 不依賴 layout（純改 attribute、不讀 bounding rect）</li>
</ul>
<h3 id="c寬範圍--subtree--全勾-option預設配置">C：寬範圍 + subtree + 全勾 option（預設配置）</h3>
<ul>
<li><strong>機制</strong>：observe(elem, { childList: true, subtree: true, attributes: true, &hellip;})</li>
<li><strong>C 是反模式</strong>：「以防萬一全勾」會觸發數十倍頻率的 callback、framework 環境必撞效能 / 競態 bug</li>
<li><strong>看起來吸引人的原因</strong>：寫法簡單、不用想要監聽什麼、「全部都看就不會漏」</li>
<li><strong>實際發生的代價</strong>：CPU 100%、layout thrashing、self-mutation 引發無限迴圈</li>
</ul>
<h3 id="ddisconnect--observe-配對處理-self-mutation">D：disconnect / observe 配對處理 self-mutation</h3>
<ul>
<li><strong>機制</strong>：apply 前 disconnect、apply 後 reconnect</li>
<li><strong>跟 A（結構分離）的取捨</strong>：D 處理 callback 必須改 observer 監聽範圍的情境、A 從設計上避免；A 更乾淨</li>
<li><strong>D 比 A 好的情境</strong>：無法做結構分離（apply 必須改 observer 看的範圍）— 唯一情境</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Observer 問題</th>
          <th>修正動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>短時間觸發數十次</td>
          <td>範圍 / option 太寬</td>
          <td>縮 root、移除不需要的 option、加 debounce</td>
      </tr>
      <tr>
          <td>Apply 跑時 layout 抖動</td>
          <td>在 framework patch 中段觸發</td>
          <td>加 debounce 50-100ms</td>
      </tr>
      <tr>
          <td>Apply 內改 DOM 進入無限循環</td>
          <td>沒處理 self-mutation</td>
          <td>用結構分離 / disconnect 配對</td>
      </tr>
      <tr>
          <td>預期變動沒觸發</td>
          <td>option 沒勾對、root 選錯</td>
          <td>對照變動類型確認 option</td>
      </tr>
      <tr>
          <td>Subtree 用了但只關心直接子</td>
          <td>過度監聽深度</td>
          <td>移除 subtree、改用直接子監聽</td>
      </tr>
      <tr>
          <td>屬性監聽觸發太頻繁</td>
          <td>沒用 attributeFilter</td>
          <td>加 filter 限縮屬性</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：MutationObserver 是非同步監聽、跟同步 selector 設計工具完全不同。範圍 / option / 頻率三維度都要顯式設計 — 預設組合會在 framework 環境中過度觸發、且難以 debug。</p>
<p><code>subtree: true</code> + <code>attributes: true</code> 是「監聽全部」的便利、窄 root + 最少 option 是「精準監聽」的對齊 — 同 <a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a> 跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 便利 vs 對齊反相關</a>。</p>
]]></content:encoded></item><item><title>setTimeout 輪詢換 MutationObserver</title><link>https://tarrragon.github.io/blog/report/mutationobserver-over-polling/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mutationobserver-over-polling/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>等待元素出現用 MutationObserver、不用 setTimeout 輪詢。&lt;/strong> Observer 是 event-driven、元素出現的瞬間觸發、無延遲；輪詢是 time-based、最快回應時間 = 輪詢間隔、且 CPU 一直跑。&lt;/p>
&lt;p>輪詢只在「沒有事件可監聽」時才用。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-observer-比輪詢好">為什麼 observer 比輪詢好&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>「等待某個 DOM 元素出現」這件事的本質是「監聽 DOM 變化、出現時觸發」 — 完全是 event-driven 場景。&lt;/p>
&lt;p>&lt;code>setTimeout&lt;/code> 輪詢的特徵：&lt;/p>
&lt;ul>
&lt;li>每隔 N ms 檢查一次、最快 N ms 才能回應&lt;/li>
&lt;li>即使元素已經出現、要等到下次檢查才知道&lt;/li>
&lt;li>CPU 持續被佔用（即使元素永遠不出現）&lt;/li>
&lt;/ul>
&lt;p>&lt;code>MutationObserver&lt;/code> 的特徵：&lt;/p>
&lt;ul>
&lt;li>元素出現的瞬間觸發 callback&lt;/li>
&lt;li>0 延遲&lt;/li>
&lt;li>DOM 沒變動時 observer 不耗 CPU&lt;/li>
&lt;/ul>
&lt;p>兩者效能差異在現代設備上不明顯、但設計上 observer 才是「適合這個場景」的工具。&lt;/p>
&lt;h3 id="何時輪詢是必要的">何時輪詢是必要的&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>輪詢必要&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>等待 DOM 元素出現&lt;/td>
 &lt;td>否 — 用 MutationObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待元素尺寸變化&lt;/td>
 &lt;td>否 — 用 ResizeObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待元素進入 viewport&lt;/td>
 &lt;td>否 — 用 IntersectionObserver&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待外部 API 結果&lt;/td>
 &lt;td>否 — 用 promise / async&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等待全局變數出現（無事件）&lt;/td>
 &lt;td>是 — 必要時輪詢&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「無事件可監聽」時才輪詢 — 這類場景在現代 Web 開發少見。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的輪詢">這次任務的輪詢&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>search.html&lt;/code> 用 setTimeout 等 pagefind UI mount：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">waitAndInit&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">filter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-panel&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">filter&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">drawer&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"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">setTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">waitAndInit&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 找到了、開始 setup
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">place&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="nx">reorderFilters&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="nx">setupScopeFilter&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="nx">waitAndInit&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每 100ms 檢查一次、有延遲、CPU 一直跑（雖然輕）。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>改用 MutationObserver 監聽 &lt;code>#search&lt;/code>（pagefind mount target）的子節點變化：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">waitForPagefind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">onReady&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 已經存在則立即觸發
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&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="nx">onReady&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 否則 observe DOM 變動
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">observer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&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">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">observer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">disconnect&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="nx">onReady&lt;/span>&lt;span class="p">();&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;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">observer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="nx">waitForPagefind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;search&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="kd">function&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">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">filter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-panel&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">reorderFilters&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>特性：&lt;/p>
&lt;ul>
&lt;li>pagefind 渲染完瞬間觸發、無延遲&lt;/li>
&lt;li>&lt;code>disconnect()&lt;/code> 後 observer 不再耗資源&lt;/li>
&lt;li>已存在時 fast path 直接觸發&lt;/li>
&lt;/ul>
&lt;h3 id="執行通用-helper">執行：通用 helper&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="cm"> * 等待 selector 在 root 內出現、觸發 callback。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="cm"> * 已存在則 sync 觸發；不存在則用 MutationObserver 等待。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">waitForElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">selector&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">callback&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"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">existing&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">root&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">selector&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">existing&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">callback&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">existing&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &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="kd">var&lt;/span> &lt;span class="nx">observer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&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">12&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">root&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">selector&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&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">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">observer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">disconnect&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">callback&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">observer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">waitForElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">searchRoot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">drawer&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">23&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 開始 setup
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把 wait 抽成 helper、setup code 變得更簡潔。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>等待元素出現用 MutationObserver、不用 setTimeout 輪詢。</strong> Observer 是 event-driven、元素出現的瞬間觸發、無延遲；輪詢是 time-based、最快回應時間 = 輪詢間隔、且 CPU 一直跑。</p>
<p>輪詢只在「沒有事件可監聽」時才用。</p>
<hr>
<h2 id="為什麼-observer-比輪詢好">為什麼 observer 比輪詢好</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>「等待某個 DOM 元素出現」這件事的本質是「監聽 DOM 變化、出現時觸發」 — 完全是 event-driven 場景。</p>
<p><code>setTimeout</code> 輪詢的特徵：</p>
<ul>
<li>每隔 N ms 檢查一次、最快 N ms 才能回應</li>
<li>即使元素已經出現、要等到下次檢查才知道</li>
<li>CPU 持續被佔用（即使元素永遠不出現）</li>
</ul>
<p><code>MutationObserver</code> 的特徵：</p>
<ul>
<li>元素出現的瞬間觸發 callback</li>
<li>0 延遲</li>
<li>DOM 沒變動時 observer 不耗 CPU</li>
</ul>
<p>兩者效能差異在現代設備上不明顯、但設計上 observer 才是「適合這個場景」的工具。</p>
<h3 id="何時輪詢是必要的">何時輪詢是必要的</h3>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>輪詢必要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>等待 DOM 元素出現</td>
          <td>否 — 用 MutationObserver</td>
      </tr>
      <tr>
          <td>等待元素尺寸變化</td>
          <td>否 — 用 ResizeObserver</td>
      </tr>
      <tr>
          <td>等待元素進入 viewport</td>
          <td>否 — 用 IntersectionObserver</td>
      </tr>
      <tr>
          <td>等待外部 API 結果</td>
          <td>否 — 用 promise / async</td>
      </tr>
      <tr>
          <td>等待全局變數出現（無事件）</td>
          <td>是 — 必要時輪詢</td>
      </tr>
  </tbody>
</table>
<p>「無事件可監聽」時才輪詢 — 這類場景在現代 Web 開發少見。</p>
<hr>
<h2 id="這次任務的輪詢">這次任務的輪詢</h2>
<h3 id="觀察">觀察</h3>
<p><code>search.html</code> 用 setTimeout 等 pagefind UI mount：</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="kd">function</span> <span class="nx">waitAndInit</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">filter</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__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <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="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">filter</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">setTimeout</span><span class="p">(</span><span class="nx">waitAndInit</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</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="c1">// 找到了、開始 setup
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nx">reorderFilters</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="nx">waitAndInit</span><span class="p">();</span></span></span></code></pre></div><p>每 100ms 檢查一次、有延遲、CPU 一直跑（雖然輕）。</p>
<h3 id="判讀">判讀</h3>
<p>改用 MutationObserver 監聽 <code>#search</code>（pagefind mount target）的子節點變化：</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="kd">function</span> <span class="nx">waitForPagefind</span><span class="p">(</span><span class="nx">searchRoot</span><span class="p">,</span> <span class="nx">onReady</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// 已經存在則立即觸發
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="k">if</span> <span class="p">(</span><span class="nx">searchRoot</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 class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">onReady</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">return</span><span class="p">;</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 class="c1">// 否則 observe DOM 變動
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">searchRoot</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 class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="nx">observer</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">      <span class="nx">onReady</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">searchRoot</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nx">waitForPagefind</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;search&#39;</span><span class="p">),</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="nx">filter</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__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <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">20</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="nx">reorderFilters</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>特性：</p>
<ul>
<li>pagefind 渲染完瞬間觸發、無延遲</li>
<li><code>disconnect()</code> 後 observer 不再耗資源</li>
<li>已存在時 fast path 直接觸發</li>
</ul>
<h3 id="執行通用-helper">執行：通用 helper</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="cm">/**
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cm"> * 等待 selector 在 root 內出現、觸發 callback。
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="cm"> * 已存在則 sync 觸發；不存在則用 MutationObserver 等待。
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kd">function</span> <span class="nx">waitForElement</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">selector</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">existing</span> <span class="o">=</span> <span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">selector</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">existing</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">callback</span><span class="p">(</span><span class="nx">existing</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="kd">var</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="kd">var</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">selector</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="nx">observer</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="nx">callback</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">// 用法
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"></span><span class="nx">waitForElement</span><span class="p">(</span><span class="nx">searchRoot</span><span class="p">,</span> <span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">drawer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="c1">// 開始 setup
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>把 wait 抽成 helper、setup code 變得更簡潔。</p>
<hr>
<h2 id="內在屬性比較四種等待機制">內在屬性比較：四種等待機制</h2>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>延遲</th>
          <th>CPU 使用</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>setTimeout</code> 單次</td>
          <td>固定延遲</td>
          <td>0</td>
          <td>等已知時間</td>
      </tr>
      <tr>
          <td><code>setTimeout</code> 輪詢</td>
          <td>平均 = 間隔 / 2</td>
          <td>持續低使用</td>
          <td>沒事件可監聽</td>
      </tr>
      <tr>
          <td><code>MutationObserver</code></td>
          <td>0 — 變動瞬間</td>
          <td>DOM 變動時短暫</td>
          <td>等待 DOM 元素</td>
      </tr>
      <tr>
          <td>Promise / async</td>
          <td>0 — resolve 瞬間</td>
          <td>0</td>
          <td>等待 async 操作</td>
      </tr>
  </tbody>
</table>
<p>優先順序：<strong>event-driven &gt; async &gt; polling &gt; timeout</strong>。輪詢是最後選擇。</p>
<hr>
<h2 id="mutationobserver-的細節">MutationObserver 的細節</h2>
<h3 id="observe-option-選對">Observe option 選對</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="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</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="nx">subtree</span><span class="o">:</span> <span class="kc">true</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="nx">attributes</span><span class="o">:</span> <span class="kc">false</span><span class="p">,</span>  <span class="c1">// 不看 attribute 變動
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">characterData</span><span class="o">:</span> <span class="kc">false</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>只勾必要的、不要全部勾 — 觸發頻率影響效能。</p>
<h3 id="找到目標後-disconnect">找到目標後 disconnect</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">var</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">found</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">observer</span><span class="p">.</span><span class="nx">disconnect</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="nx">callback</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></span></code></pre></div><p>不 disconnect 的話、observer 一直 active、未來任何 DOM 變動都觸發 callback。</p>
<h3 id="已存在的-fast-path">已存在的 fast path</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="k">if</span> <span class="p">(</span><span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">selector</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">callback</span><span class="p">();</span>   <span class="c1">// 已存在則直接觸發、不需 observer
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>避免「元素已經存在但還是要等下次變動才觸發」的延遲。</p>
<hr>
<h2 id="設計取捨等待-dom-元素出現的策略">設計取捨：等待 DOM 元素出現的策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（MutationObserver + fast path）當預設、其他做法在特定情境合理。</p>
<h3 id="amutationobserver--already-exists-fast-path這個專案的預設">A：MutationObserver + already-exists fast path（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：先檢查目標是否已存在（直接觸發）、否則 observe DOM 變動、找到後 disconnect</li>
<li><strong>選 A 的理由</strong>：0 延遲、CPU 不被輪詢吃、找到後立即停</li>
<li><strong>適合</strong>：等待 framework / 第三方 library 動態 mount 的元素</li>
<li><strong>代價</strong>：需要寫 fast path + observer + disconnect 三段邏輯（用 helper 包裝即可一行調用）</li>
</ul>
<h3 id="bsettimeout-輪詢">B：<code>setTimeout</code> 輪詢</h3>
<ul>
<li><strong>機制</strong>：每隔 N ms 檢查、找到就停</li>
<li><strong>跟 A 的取捨</strong>：B 寫法簡單、A 設計嚴謹；但 B 有最快回應 = N ms 的延遲、CPU 一直跑</li>
<li><strong>B 比 A 好的情境</strong>：等待對象是無事件可監聽的狀態（全局變數出現、外部 API 結果且無 promise 介面），MutationObserver 無處掛載</li>
</ul>
<h3 id="cpromise--async如果-api-提供">C：Promise / async（如果 API 提供）</h3>
<ul>
<li><strong>機制</strong>：<code>await framework.ready()</code> 等 framework 提供的 promise</li>
<li><strong>跟 A 的取捨</strong>：C 是最乾淨的解、但需要 framework / library 提供 promise API</li>
<li><strong>C 比 A 好的情境</strong>：等的目標有官方 promise 介面（避免自行 observe 內部 DOM）</li>
</ul>
<h3 id="drequestanimationframe-迴圈">D：<code>requestAnimationFrame</code> 迴圈</h3>
<ul>
<li><strong>機制</strong>：每個 frame 檢查一次</li>
<li><strong>跟 B 的取捨</strong>：D 跟著 frame、不會在 idle 時跑；但仍是輪詢、延遲 16ms</li>
<li><strong>D 才合理的情境</strong>：等待動畫 frame 相關狀態（罕見）— 純等 DOM 元素仍應用 A</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>setTimeout</code> 用來等 DOM 元素</td>
          <td>改 <code>MutationObserver</code> + disconnect</td>
      </tr>
      <tr>
          <td><code>setInterval</code> 不停跑檢查元素狀態</td>
          <td>改 <code>MutationObserver</code> 或 <code>ResizeObserver</code></td>
      </tr>
      <tr>
          <td>等待邏輯有「最快 X ms 才回應」的延遲</td>
          <td>改 event-driven 機制、消除延遲</td>
      </tr>
      <tr>
          <td>Observer 找到目標後沒 disconnect</td>
          <td>加 disconnect、避免繼續觸發</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：DOM 變動有對應的 event 機制可監聽 — 用對機制就有 0 延遲、無 CPU 浪費。輪詢是「沒辦法的辦法」、不是 default。</p>
]]></content:encoded></item><item><title>Init function 是 orchestrator、職責拆出獨立 function</title><link>https://tarrragon.github.io/blog/report/split-setup-by-responsibility/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/split-setup-by-responsibility/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>單一函式做 ≥ 3 件無關的事就拆。&lt;/strong> 每個函式只負責一個職責、有明確的 input / output、可以獨立 debug 與測試。Init function 變成「組合各職責 function 的 orchestrator」。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一函式 50 行做 1 件事&lt;/td>
 &lt;td>低 — 容易讀、職責清楚&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一函式 100 行做 3 件事&lt;/td>
 &lt;td>中 — 邏輯交織、debug 要分辨哪段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一函式 200 行做 5+ 件事&lt;/td>
 &lt;td>高 — 沒人想動、改一處可能影響別處&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>拆函式的成本是「多寫幾個函式名與簽名」、收益是「每個函式範圍小、debug 容易、可單獨重用」。&lt;/p>
&lt;h3 id="拆的依據是職責不是行數">拆的依據是「職責」、不是行數&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>拆法&lt;/th>
 &lt;th>結果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>按行數機械拆&lt;/td>
 &lt;td>切出沒邏輯意義的片段、更亂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>按職責拆&lt;/td>
 &lt;td>每個函式名能描述「做什麼」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>按 input / output 拆&lt;/td>
 &lt;td>函式變得 testable、可組合&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>按職責拆的判斷：能不能用一個動詞片語描述函式做什麼？做不到 → 多個職責、該拆。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的拆分機會">這次任務的拆分機會&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>setupScopeFilter()&lt;/code> 現況做 5 件事：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupScopeFilter&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 1. 找元素
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">scopeEl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 2. 量測 scope 高度寫回 CSS 變數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&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"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&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">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&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>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 3. 把 filter-panel 搬到 sidebar (position function)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&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">14&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 4. 註冊 scope filter listener + apply
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">apply&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&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">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">apply&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 5. Reorder filter blocks
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">reorderFilters&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&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">22&lt;/span>&lt;span class="cl"> &lt;span class="nx">reorderFilters&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>5 個職責塞在一個函式：找元素、量高度、搬 slot、scope filter、reorder filter。&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-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">findSearchElements&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">shell&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">ui&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">filter&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-panel&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">scope&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">),&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&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">13&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">update&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">14&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">offsetHeight&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="mi">56&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setProperty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;--search-scope-h&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">h&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="nx">update&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">update&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupFilterSlotSwap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">slot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">breakpoint&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">22&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">breakpoint&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;px)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">place&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">24&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">slot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl"> &lt;span class="k">else&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl"> &lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl"> &lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">30&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">31&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">reorderFilters&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">desiredOrder&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">32&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">blocks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-block&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">33&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">byKey&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">34&lt;/span>&lt;span class="cl"> &lt;span class="nx">blocks&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">b&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">35&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__filter-name&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">textContent&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">trim&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nx">toLowerCase&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">36&lt;/span>&lt;span class="cl"> &lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">key&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">37&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">38&lt;/span>&lt;span class="cl"> &lt;span class="nx">desiredOrder&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">k&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">39&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">byKey&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">]);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">40&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">41&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">42&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">43&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ui&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">44&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">getScope&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&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">45&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">apply&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&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">46&lt;/span>&lt;span class="cl"> &lt;span class="kd">function&lt;/span> &lt;span class="nx">schedule&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&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">47&lt;/span>&lt;span class="cl"> &lt;span class="nx">scopeEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">schedule&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">48&lt;/span>&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">schedule&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">49&lt;/span>&lt;span class="cl"> &lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">schedule&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">50&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>init()&lt;/code> 變成 orchestrator：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">init&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 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 class="nx">waitForElement&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&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"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">els&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">findSearchElements&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">scope&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlotSwap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">drawer&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="mi">1400&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">reorderFilters&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;type&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;tag&amp;#39;&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="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">scope&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">els&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ui&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="p">});&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;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nx">init&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個拆出的函式：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>單一函式做 ≥ 3 件無關的事就拆。</strong> 每個函式只負責一個職責、有明確的 input / output、可以獨立 debug 與測試。Init function 變成「組合各職責 function 的 orchestrator」。</p>
<hr>
<h2 id="為什麼拆函式">為什麼拆函式</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>單一函式做多件事的成本：</p>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>維護痛點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一函式 50 行做 1 件事</td>
          <td>低 — 容易讀、職責清楚</td>
      </tr>
      <tr>
          <td>一函式 100 行做 3 件事</td>
          <td>中 — 邏輯交織、debug 要分辨哪段</td>
      </tr>
      <tr>
          <td>一函式 200 行做 5+ 件事</td>
          <td>高 — 沒人想動、改一處可能影響別處</td>
      </tr>
  </tbody>
</table>
<p>拆函式的成本是「多寫幾個函式名與簽名」、收益是「每個函式範圍小、debug 容易、可單獨重用」。</p>
<h3 id="拆的依據是職責不是行數">拆的依據是「職責」、不是行數</h3>
<table>
  <thead>
      <tr>
          <th>拆法</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>按行數機械拆</td>
          <td>切出沒邏輯意義的片段、更亂</td>
      </tr>
      <tr>
          <td>按職責拆</td>
          <td>每個函式名能描述「做什麼」</td>
      </tr>
      <tr>
          <td>按 input / output 拆</td>
          <td>函式變得 testable、可組合</td>
      </tr>
  </tbody>
</table>
<p>按職責拆的判斷：能不能用一個動詞片語描述函式做什麼？做不到 → 多個職責、該拆。</p>
<hr>
<h2 id="這次任務的拆分機會">這次任務的拆分機會</h2>
<h3 id="觀察">觀察</h3>
<p><code>setupScopeFilter()</code> 現況做 5 件事：</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="kd">function</span> <span class="nx">setupScopeFilter</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// 1. 找元素
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">scopeEl</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"> 4</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</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__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 2. 量測 scope 高度寫回 CSS 變數
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">syncScopeHeight</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="c1">// 3. 把 filter-panel 搬到 sidebar (position function)
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="c1">// 4. 註冊 scope filter listener + apply
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">apply</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="c1">// 5. Reorder filter blocks
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">reorderFilters</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="nx">reorderFilters</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>5 個職責塞在一個函式：找元素、量高度、搬 slot、scope filter、reorder filter。</p>
<h3 id="判讀">判讀</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="kd">function</span> <span class="nx">findSearchElements</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">shell</span><span class="o">:</span>  <span class="nx">shell</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">ui</span><span class="o">:</span>     <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">input</span><span class="o">:</span>  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="o">:</span> <span class="nx">shell</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"> 7</span><span class="cl">    <span class="nx">filter</span><span class="o">:</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">scope</span><span class="o">:</span>  <span class="nx">shell</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"> 9</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kd">function</span> <span class="nx">update</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="kd">var</span> <span class="nx">h</span> <span class="o">=</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">||</span> <span class="mi">56</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</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">16</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nx">update</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">update</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="kd">function</span> <span class="nx">setupFilterSlotSwap</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">,</span> <span class="nx">slot</span><span class="p">,</span> <span class="nx">breakpoint</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: &#39;</span> <span class="o">+</span> <span class="nx">breakpoint</span> <span class="o">+</span> <span class="s1">&#39;px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">slot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="kd">function</span> <span class="nx">reorderFilters</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">,</span> <span class="nx">desiredOrder</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="kd">var</span> <span class="nx">blocks</span> <span class="o">=</span> <span class="nx">filterPanel</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-block&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">  <span class="kd">var</span> <span class="nx">byKey</span> <span class="o">=</span> <span class="p">{};</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">  <span class="nx">blocks</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">b</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">b</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-name&#39;</span><span class="p">).</span><span class="nx">textContent</span><span class="p">.</span><span class="nx">trim</span><span class="p">().</span><span class="nx">toLowerCase</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="nx">byKey</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="o">=</span> <span class="nx">b</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">  <span class="nx">desiredOrder</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">])</span> <span class="nx">filterPanel</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="kd">function</span> <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">,</span> <span class="nx">input</span><span class="p">,</span> <span class="nx">ui</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">  <span class="kd">function</span> <span class="nx">getScope</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">  <span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">  <span class="kd">function</span> <span class="nx">schedule</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">  <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">schedule</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">  <span class="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="nx">schedule</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">  <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">schedule</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">ui</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>init()</code> 變成 orchestrator：</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="kd">function</span> <span class="nx">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</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-shell&#39;</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="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">waitForElement</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">els</span> <span class="o">=</span> <span class="nx">findSearchElements</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">scope</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">setupFilterSlotSwap</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">els</span><span class="p">.</span><span class="nx">drawer</span><span class="p">,</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">),</span> <span class="mi">1400</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">reorderFilters</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">filter</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">,</span> <span class="s1">&#39;tag&#39;</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">els</span><span class="p">.</span><span class="nx">scope</span><span class="p">,</span> <span class="nx">els</span><span class="p">.</span><span class="nx">input</span><span class="p">,</span> <span class="nx">els</span><span class="p">.</span><span class="nx">ui</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nx">init</span><span class="p">();</span></span></span></code></pre></div><p>每個拆出的函式：</p>
<ul>
<li>名字描述做什麼（動詞 + 名詞）</li>
<li>接受需要的元素當參數（不依賴全局）</li>
<li>不知道其他函式的存在（解耦）</li>
</ul>
<hr>
<h2 id="內在屬性比較四種函式拆分粒度">內在屬性比較：四種函式拆分粒度</h2>
<table>
  <thead>
      <tr>
          <th>粒度</th>
          <th>維護成本</th>
          <th>Debug 範圍</th>
          <th>可重用性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一個 mega init function</td>
          <td>高 — 200+ 行交織</td>
          <td>整個函式都要看</td>
          <td>低 — 跟特定 setup 綁</td>
      </tr>
      <tr>
          <td>按行數機械拆（每 30 行一份）</td>
          <td>中 — 切出無意義片段</td>
          <td>中</td>
          <td>低</td>
      </tr>
      <tr>
          <td>按職責拆</td>
          <td>低 — 每函式單一職責</td>
          <td>函式內部、範圍小</td>
          <td>高</td>
      </tr>
      <tr>
          <td>按職責拆 + class 包裝</td>
          <td>低</td>
          <td>範圍小</td>
          <td>最高 — 多實例</td>
      </tr>
  </tbody>
</table>
<p>優先按職責拆 — 函式名表達 intent、debug 範圍小、單獨可測。</p>
<hr>
<h2 id="拆函式的具體技巧">拆函式的具體技巧</h2>
<h3 id="1-函式名是動詞片語">1. 函式名是動詞片語</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="nx">syncScopeHeight</span><span class="p">()</span>           <span class="c1">// 動詞 + 對象
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">setupFilterSlotSwap</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="nx">reorderFilters</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="nx">findSearchElements</span><span class="p">()</span>        <span class="c1">// 動詞 + 對象
</span></span></span></code></pre></div><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="nx">filter</span><span class="p">()</span>        <span class="c1">// 動詞模糊（filter 是動詞還是名詞？）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">handle</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="nx">init</span><span class="p">()</span>          <span class="c1">// 只有 orchestrator 用、不要散在各處
</span></span></span></code></pre></div><h3 id="2-參數是該函式需要的不傳一個-mega-object">2. 參數是該函式需要的、不傳一個 mega object</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="c1">// 好 — 函式知道它需要什麼
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">)</span> <span class="p">{</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="c1">// 較差 — 函式拿到一堆無關的東西、不清楚依賴
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">allElements</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">allElements</span><span class="p">.</span><span class="nx">scope</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></code></pre></div><p>明確參數 = 明確依賴 = 容易測試。</p>
<h3 id="3-副作用集中在一處">3. 副作用集中在一處</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">syncScopeHeight</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">function</span> <span class="nx">update</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</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">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">update</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">update</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>副作用（DOM 變動、event listener、observer）都在這個函式內。沒散到別處。</p>
<h3 id="4-不依賴外部變數">4. 不依賴外部變數</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="c1">// 好 — 純函式、依賴只在參數
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">reorderFilters</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">,</span> <span class="nx">desiredOrder</span><span class="p">)</span> <span class="p">{</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="c1">// 較差 — 依賴外部全局變數
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">desiredOrder</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;type&#39;</span><span class="p">,</span> <span class="s1">&#39;tag&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="kd">function</span> <span class="nx">reorderFilters</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="c1">// 用了 desiredOrder
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>純函式 = 無隱式依賴 = 重用方便、測試方便。</p>
<hr>
<h2 id="設計取捨大型-init-function-的拆分策略">設計取捨：大型 init function 的拆分策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（按職責拆 + 純函式）當預設、其他做法在特定情境合理。</p>
<h3 id="a按職責拆--純函式--init-當-orchestrator這個專案的預設">A：按職責拆 + 純函式 + init 當 orchestrator（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：每職責一個函式（動詞 + 對象命名）、依賴透過參數傳入、init 組合各函式</li>
<li><strong>選 A 的理由</strong>：debug 範圍小（職責 = 函式 = grep 範圍）、單獨可測、可重用</li>
<li><strong>適合</strong>：&gt; 50 行的 init function、預期長期維護</li>
<li><strong>代價</strong>：多寫幾個函式名與簽名、檔案 LOC 略增</li>
</ul>
<h3 id="b按行數機械拆每-30-行一份">B：按行數機械拆（每 30 行一份）</h3>
<ul>
<li><strong>機制</strong>：固定 LOC 拆檔、不考慮職責邊界</li>
<li><strong>跟 A 的取捨</strong>：B 拆完後切片無邏輯意義、A 切片各自完整；B 更亂、debug 反而更難</li>
<li><strong>B 是反模式</strong>：「行數」不是有意義的拆分判準 — 拆完後切片無邏輯意義、debug 反而更難</li>
</ul>
<h3 id="c保持-mega-init-function">C：保持 mega init function</h3>
<ul>
<li><strong>機制</strong>：所有 setup 邏輯塞在一個 init 內</li>
<li><strong>跟 A 的取捨</strong>：C 一個函式看完所有 setup、A 散在多函式；但 C 在 200+ 行時改一處要小心整體</li>
<li><strong>C 才合理的情境</strong>：&lt; 50 行的 init、職責本來就單一</li>
</ul>
<h3 id="d按職責拆--class-包裝多實例">D：按職責拆 + class 包裝多實例</h3>
<ul>
<li><strong>機制</strong>：把 setup 包成 class、<code>new SearchShell(rootEl)</code> 建立實例</li>
<li><strong>跟 A 的取捨</strong>：D 多實例支援更乾淨（每實例自己的 state）、A 用純函式 + 起點當參數也能達成</li>
<li><strong>D 比 A 好的情境</strong>：元件有複雜的內部 state、預期會被多次實例化（library 設計）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一個函式 100+ 行</td>
          <td>列出做的事、按職責拆</td>
      </tr>
      <tr>
          <td>函式名抽象（<code>init</code> / <code>handle</code> / <code>process</code>）</td>
          <td>改名動詞 + 對象、表達 intent</td>
      </tr>
      <tr>
          <td>函式內讀外部全局變數</td>
          <td>把依賴改為參數、純函式化</td>
      </tr>
      <tr>
          <td>Debug 時要 grep 整個函式找哪段邏輯</td>
          <td>拆完後職責 = 函式 = grep 範圍縮小</td>
      </tr>
      <tr>
          <td>同一段邏輯複製到別處</td>
          <td>拆成獨立函式、兩處引用</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：函式是「做一件事」的單位。一個函式越多職責、debug 與重用越難。拆 = 投資、回報是未來的維護成本下降。</p>
]]></content:encoded></item><item><title>baseof.html override 範圍最小化</title><link>https://tarrragon.github.io/blog/report/minimize-baseof-override/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/minimize-baseof-override/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Override theme 檔案的範圍越小、theme 升級時越容易 sync。&lt;/strong> 整檔 copy + 改 1-2 行的 override 在 theme 改了 baseof 時、本地必須手動 merge；只 override 必要的部分（用 partial 或最小檔案）讓變更面積小、merge 容易。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-override-要小">為什麼 override 要小&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Hugo theme 的 lookup order：本地 &lt;code>layouts/&lt;/code> 優先於 &lt;code>themes/&amp;lt;name&amp;gt;/layouts/&lt;/code>。本地有同名檔案、本地的整個內容生效、theme 的版本完全被忽略。&lt;/p>
&lt;p>當本地 override 整個 baseof.html、只改 1-2 行：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Theme 升級時的代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>本地 override 不會自動更新 — 永遠是當初 copy 的版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Theme 的新功能（新 partial、改進的 SEO meta）不會生效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>要手動 diff &lt;code>themes/&amp;lt;name&amp;gt;/layouts/baseof.html&lt;/code> 與本地、合併變更&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容易忘記、theme 的修正在本地永遠沒套到&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Override 範圍小 = merge 面積小 = 升級時手動 sync 的工作量小。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的-override">這次任務的 override&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>&lt;code>layouts/_default/baseof.html&lt;/code> 是整個 theme 的 baseof 複製、只改了：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-diff" data-lang="diff">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gd">- &amp;lt;body&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gd">&lt;/span>&lt;span class="gi">+ &amp;lt;body{{ if eq .Layout &amp;#34;search&amp;#34; }} class=&amp;#34;page-search&amp;#34;{{ end }}&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="gi">+ {{- partial &amp;#34;pagefind_meta.html&amp;#34; . -}}
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個改動：&lt;/p>
&lt;ol>
&lt;li>&lt;code>&amp;lt;body&amp;gt;&lt;/code> 加條件 class（搜尋頁需要的 hook）&lt;/li>
&lt;li>&lt;code>&amp;lt;main&amp;gt;&lt;/code> 內加 &lt;code>pagefind_meta.html&lt;/code> partial 引用&lt;/li>
&lt;/ol>
&lt;p>整個 baseof（44 行）完全 copy、只為了這兩處 5 行改動。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>兩個改動都有更小的 override 方式：&lt;/p>
&lt;h4 id="改動-1body-class">改動 1：body class&lt;/h4>
&lt;p>Hugo 的 &lt;code>block&lt;/code> 機制讓 child template override &lt;code>block&lt;/code> 內容。如果 theme baseof 預先定義了 &lt;code>body-class&lt;/code> block：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- theme baseof.html --&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;{{ block &amp;#34;&lt;/span>&lt;span class="na">body-class&lt;/span>&lt;span class="err">&amp;#34;&lt;/span> &lt;span class="err">.&lt;/span> &lt;span class="err">}}{{&lt;/span> &lt;span class="na">end&lt;/span> &lt;span class="err">}}&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>那本地搜尋頁 layout 可以：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- layouts/_default/search.html --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">{{ define &amp;#34;body-class&amp;#34; }}page-search{{ end }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不需要 override 整個 baseof。&lt;/p>
&lt;p>但這次的 theme 沒有 &lt;code>body-class&lt;/code> block — 所以這條路不通、必須 override。&lt;/p>
&lt;p>替代：用 &lt;code>custom_body.html&lt;/code> 之類已有的 partial hook。Theme 可能在 body 結束前 inject &lt;code>custom_body.html&lt;/code>、但那發生在 body 開始之後、無法影響 body 的 attribute。&lt;/p>
&lt;p>結論：第一個改動需要 override baseof、無更小的方式。&lt;/p>
&lt;h4 id="改動-2pagefind_metahtml-partial">改動 2：pagefind_meta.html partial&lt;/h4>
&lt;p>這個 partial 注入在 &lt;code>&amp;lt;main&amp;gt;&lt;/code> 開頭、加 hidden filter spans。可以放到 &lt;code>custom_head.html&lt;/code>（theme 已有的 hook） — 但 head 內的元素不會被 pagefind 索引、所以那條路不通。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Override theme 檔案的範圍越小、theme 升級時越容易 sync。</strong> 整檔 copy + 改 1-2 行的 override 在 theme 改了 baseof 時、本地必須手動 merge；只 override 必要的部分（用 partial 或最小檔案）讓變更面積小、merge 容易。</p>
<hr>
<h2 id="為什麼-override-要小">為什麼 override 要小</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Hugo theme 的 lookup order：本地 <code>layouts/</code> 優先於 <code>themes/&lt;name&gt;/layouts/</code>。本地有同名檔案、本地的整個內容生效、theme 的版本完全被忽略。</p>
<p>當本地 override 整個 baseof.html、只改 1-2 行：</p>
<table>
  <thead>
      <tr>
          <th>Theme 升級時的代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本地 override 不會自動更新 — 永遠是當初 copy 的版本</td>
      </tr>
      <tr>
          <td>Theme 的新功能（新 partial、改進的 SEO meta）不會生效</td>
      </tr>
      <tr>
          <td>要手動 diff <code>themes/&lt;name&gt;/layouts/baseof.html</code> 與本地、合併變更</td>
      </tr>
      <tr>
          <td>容易忘記、theme 的修正在本地永遠沒套到</td>
      </tr>
  </tbody>
</table>
<p>Override 範圍小 = merge 面積小 = 升級時手動 sync 的工作量小。</p>
<hr>
<h2 id="這次任務的-override">這次任務的 override</h2>
<h3 id="觀察">觀察</h3>
<p><code>layouts/_default/baseof.html</code> 是整個 theme 的 baseof 複製、只改了：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="ln">1</span><span class="cl"><span class="gd">- &lt;body&gt;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gd"></span><span class="gi">+ &lt;body{{ if eq .Layout &#34;search&#34; }} class=&#34;page-search&#34;{{ end }}&gt;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="gi">+   {{- partial &#34;pagefind_meta.html&#34; . -}}
</span></span></span></code></pre></div><p>兩個改動：</p>
<ol>
<li><code>&lt;body&gt;</code> 加條件 class（搜尋頁需要的 hook）</li>
<li><code>&lt;main&gt;</code> 內加 <code>pagefind_meta.html</code> partial 引用</li>
</ol>
<p>整個 baseof（44 行）完全 copy、只為了這兩處 5 行改動。</p>
<h3 id="判讀">判讀</h3>
<p>兩個改動都有更小的 override 方式：</p>
<h4 id="改動-1body-class">改動 1：body class</h4>
<p>Hugo 的 <code>block</code> 機制讓 child template override <code>block</code> 內容。如果 theme baseof 預先定義了 <code>body-class</code> block：</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;!-- theme baseof.html --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">body</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;{{ block &#34;</span><span class="na">body-class</span><span class="err">&#34;</span> <span class="err">.</span> <span class="err">}}{{</span> <span class="na">end</span> <span class="err">}}&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>那本地搜尋頁 layout 可以：</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;!-- layouts/_default/search.html --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ define &#34;body-class&#34; }}page-search{{ end }}</span></span></code></pre></div><p>不需要 override 整個 baseof。</p>
<p>但這次的 theme 沒有 <code>body-class</code> block — 所以這條路不通、必須 override。</p>
<p>替代：用 <code>custom_body.html</code> 之類已有的 partial hook。Theme 可能在 body 結束前 inject <code>custom_body.html</code>、但那發生在 body 開始之後、無法影響 body 的 attribute。</p>
<p>結論：第一個改動需要 override baseof、無更小的方式。</p>
<h4 id="改動-2pagefind_metahtml-partial">改動 2：pagefind_meta.html partial</h4>
<p>這個 partial 注入在 <code>&lt;main&gt;</code> 開頭、加 hidden filter spans。可以放到 <code>custom_head.html</code>（theme 已有的 hook） — 但 head 內的元素不會被 pagefind 索引、所以那條路不通。</p>
<p>也可以從每個 layout 內手動引入：</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;!-- layouts/_default/single.html --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">{{ define &#34;main&#34; }}
</span></span><span class="line"><span class="ln">3</span><span class="cl">{{- partial &#34;pagefind_meta.html&#34; . -}}
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>{{ .Title }}<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">...
</span></span><span class="line"><span class="ln">6</span><span class="cl">{{ end }}</span></span></code></pre></div><p>但這樣每個 layout（single、list、search、taxonomy）都要重複引用 — 維護成本不一定更低。</p>
<p>結論：第二個改動放在 baseof 比放在每個 layout 更乾淨。</p>
<h3 id="執行當前-override-已是最小">執行：當前 override 已是最小</h3>
<p>兩個改動都是 baseof override 較合理。但可以做的精簡是 <strong>註解標明跟 theme 版本的差異</strong>：</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></span><span class="line"><span class="ln"> 2</span><span class="cl">  本地 override theme baseof.html。
</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">  跟 themes/hugo-bearcub/layouts/_default/baseof.html 的差異：
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    1. <span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span> 加條件 class=&#34;page-search&#34;（給搜尋頁的 CSS / JS hook 用）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    2. <span class="p">&lt;</span><span class="nt">main</span><span class="p">&gt;</span> 內加 partial &#34;pagefind_meta.html&#34;（注入 pagefind filter metadata）
</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">  Theme 升級時、把上面兩個改動套到新版 baseof 即可。
</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="cp">&lt;!DOCTYPE html&gt;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;...&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">...</span></span></code></pre></div><p>註解告訴未來的維護者「這檔案 override 了什麼、為什麼、升級時要看哪些 diff」。</p>
<hr>
<h2 id="內在屬性比較四種-override-策略">內在屬性比較：四種 override 策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>改動面積</th>
          <th>升級成本</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>整檔 copy + 修改</td>
          <td>大</td>
          <td>高 — 手動 merge 整檔</td>
          <td>Theme 沒提供 hook、必要</td>
      </tr>
      <tr>
          <td>Override 加註解標明 diff</td>
          <td>大</td>
          <td>中 — 註解告訴升級者改了什麼</td>
          <td>整檔 override 的最佳實踐</td>
      </tr>
      <tr>
          <td>用 theme 提供的 partial / block hook</td>
          <td>小</td>
          <td>低 — theme 升級不影響</td>
          <td>Theme 設計時預留了 hook</td>
      </tr>
      <tr>
          <td>Fork theme 並維護</td>
          <td>整個 theme</td>
          <td>最高 — 整個 theme 都要 sync</td>
          <td>客製極深、theme 沒 hook</td>
      </tr>
  </tbody>
</table>
<p>優先選「用 theme 提供的 hook」、次選「override 加註解」、最後才考慮 fork。</p>
<hr>
<h2 id="override-的具體最佳實踐">Override 的具體最佳實踐</h2>
<h3 id="1-註解標明-diff">1. 註解標明 diff</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></span><span class="line"><span class="ln">2</span><span class="cl">  Override theme/.../baseof.html。
</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="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span> 加 class hook
</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> 內加 partial
</span></span><span class="line"><span class="ln">6</span><span class="cl">*/ -}}</span></span></code></pre></div><p>註解讓未來維護者一眼知道改了什麼。</p>
<h3 id="2-override-檔案內容對齊-theme-版本">2. Override 檔案內容對齊 theme 版本</h3>
<p>當 theme 升級時：</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">diff themes/hugo-bearcub/layouts/_default/baseof.html <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>     layouts/_default/baseof.html</span></span></code></pre></div><p>差異應該只有註解內標明的那幾處。如果差異更多 — 表示 theme 有變更我們沒套到。</p>
<h3 id="3-標明-theme-版本">3. 標明 theme 版本</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></span><span class="line"><span class="ln">2</span><span class="cl">  Override based on themes/hugo-bearcub@v1.2.3.
</span></span><span class="line"><span class="ln">3</span><span class="cl">  跟該版本的 baseof.html 差異：...
</span></span><span class="line"><span class="ln">4</span><span class="cl">*/ -}}</span></span></code></pre></div><p>知道是基於哪個版本 override、升級到 v1.3.0 時知道要 diff 哪兩個版本。</p>
<h3 id="4-主動建議-theme-加-hook">4. 主動建議 theme 加 hook</h3>
<p>如果常需要 override theme 同樣的位置、考慮給 theme 提 PR 加 <code>block</code> 或 <code>partial</code> hook — 這樣升級後 hook 自動有、不需要繼續 override。</p>
<hr>
<h2 id="設計取捨theme-客製的策略">設計取捨：Theme 客製的策略</h2>
<p>四種做法、各自機會成本不同。優先選 A（用 theme hook）— 不夠用才退到 B / C / D。</p>
<h3 id="a用-theme-提供的-partial--block--template-hook最佳">A：用 theme 提供的 partial / block / template hook（最佳）</h3>
<ul>
<li><strong>機制</strong>：theme 預留 <code>block</code>、<code>custom_head.html</code>、<code>custom_body.html</code> 等 hook、本地只填 hook</li>
<li><strong>選 A 的理由</strong>：theme 升級不影響本地客製、hook 是公開介面</li>
<li><strong>適合</strong>：theme 設計時預留了對應 hook 的客製需求</li>
<li><strong>代價</strong>：需要 theme 預先支援、若不支援考慮給 theme 提 PR 加 hook</li>
</ul>
<h3 id="boverride-加-diff-註解這個專案的預設">B：Override 加 diff 註解（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：複製 theme 檔案到本地、改必要的部分、註解標明跟 theme 版本的差異</li>
<li><strong>跟 A 的取捨</strong>：B 不需要 theme 預留 hook、A 需要；B 升級時要手動 sync</li>
<li><strong>適合</strong>：theme 沒對應 hook、必須 override</li>
<li><strong>代價</strong>：升級時要 diff theme 新版手動 merge、註解可降低 merge 成本</li>
</ul>
<h3 id="coverride-不加註解">C：Override 不加註解</h3>
<ul>
<li><strong>機制</strong>：複製 theme 檔案、改必要部分、不註解</li>
<li><strong>跟 B 的取捨</strong>：C 寫法簡單、B 額外註解；但 C 未來維護者不知為什麼這檔案在本地、漏 sync 風險高</li>
<li><strong>C 才合理的情境</strong>：純探索性 override、之後會還原 — production 不該如此</li>
</ul>
<h3 id="dfork-theme-維護自己版本">D：Fork theme 維護自己版本</h3>
<ul>
<li><strong>機制</strong>：fork theme 整個 repo、所有客製改在 fork 內</li>
<li><strong>成本特別高的原因</strong>：每次原 theme 升級都要 merge upstream、長期維護負擔重</li>
<li><strong>D 才合理的情境</strong>：客製極深（多檔案 override + 改 internal logic）、且願意承擔 fork 維護成本</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>整檔 override theme 檔案、只改 1-2 行</td>
          <td>加註解標明 diff、未來容易升級</td>
      </tr>
      <tr>
          <td>Override 不知道是基於哪個 theme 版本</td>
          <td>加版本註解</td>
      </tr>
      <tr>
          <td>Theme 升級後本地客製失效 / 出怪事</td>
          <td>Diff theme 新版與本地 override、手動 sync</td>
      </tr>
      <tr>
          <td>多個 override 檔案、不知道為什麼存在</td>
          <td>每個 override 加用途註解</td>
      </tr>
      <tr>
          <td>同樣的客製需求要 override 多個檔案</td>
          <td>評估給 theme 提 PR 加 hook</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Override 是雙面刃 — 短期解決客製、長期增加升級成本。把 override 範圍與 diff 範圍維持最小、註解說明來由 — 是長期可維護的妥協。</p>
]]></content:encoded></item><item><title>Reactive 監聽器的效能 audit：跨 listener 類型盤點觸發頻率</title><link>https://tarrragon.github.io/blog/report/reactive-listener-frequency-management/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/reactive-listener-frequency-management/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>監聽器的「觸發頻率」是效能的第一道防線、跨多種 listener 類型一起盤點。&lt;/strong> 本篇是 audit 視角（「我有效能問題、reactive 監聽器是不是嫌疑」）— 設計新 observer 的細節由 &lt;a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率&lt;/a> 處理。Audit 時把所有 reactive 監聽器列一張表、看哪些觸發頻率異常。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>跨 listener 類型的效能盤點&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MutationObserver 的設計細節&lt;/strong>（root / option / debounce / self-mutation）由 &lt;a href="../mutation-observer-scope/">#29&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>Selector 範圍的設計&lt;/strong>由 &lt;a href="../dom-selector-precision/">#14&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>Runtime 計算成本&lt;/strong>（regex / textContent / forEach）由 &lt;a href="../runtime-iteration-and-regex-cost/">#34&lt;/a> 處理&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼觸發頻率主導效能">為什麼觸發頻率主導效能&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Reactive 監聽器有三個獨立成本：&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>看範圍與 option&lt;/td>
 &lt;td>倍數疊加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Callback 內部運算&lt;/td>
 &lt;td>看實作&lt;/td>
 &lt;td>每次完整跑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Callback 引發的副作用&lt;/td>
 &lt;td>看 DOM 變動&lt;/td>
 &lt;td>可能反向觸發&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把單次 callback 從 5ms 優化到 2ms 是 2.5x；把觸發次數從 100 次/秒降到 10 次/秒是 10x。&lt;strong>觸發頻率優化的天花板更高&lt;/strong> — audit 時優先看頻率。&lt;/p>
&lt;h3 id="三類觸發頻率風險速覽">三類觸發頻率風險（速覽）&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>詳細處理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>範圍過寬（observer subtree）&lt;/td>
 &lt;td>無關變動也觸發&lt;/td>
 &lt;td>&lt;a href="../mutation-observer-scope/">#29 root 與 option 設計&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Option 全勾&lt;/td>
 &lt;td>多種變動類型同時觸發&lt;/td>
 &lt;td>&lt;a href="../mutation-observer-scope/">#29 三維度收斂&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自激迴圈&lt;/td>
 &lt;td>callback 自己改 DOM 觸發自己&lt;/td>
 &lt;td>&lt;a href="../mutation-observer-scope/">#29 self-mutation 處理&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>本篇不展開設計細節（避免跟 #29 重複）、只談「audit 時怎麼識別這些 risk」。&lt;/p>
&lt;hr>
&lt;h2 id="跨-observer-類型的盤點">跨 observer 類型的盤點&lt;/h2>
&lt;p>效能 audit 時、列出&lt;strong>所有&lt;/strong> reactive 監聽器、不只 MutationObserver。各類型觸發來源不同、需要分別評估。&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>MutationObserver&lt;/td>
 &lt;td>DOM 變動&lt;/td>
 &lt;td>一次操作觸發 10+ 次&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ResizeObserver&lt;/td>
 &lt;td>元素尺寸變動&lt;/td>
 &lt;td>持續觸發（自激）/ resize 視窗時連發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IntersectionObserver&lt;/td>
 &lt;td>可視性變動&lt;/td>
 &lt;td>scroll 時連發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Event listener (input / scroll / resize)&lt;/td>
 &lt;td>使用者互動&lt;/td>
 &lt;td>高頻事件未 debounce&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>setInterval&lt;/code> / &lt;code>requestAnimationFrame&lt;/code> 迴圈&lt;/td>
 &lt;td>時間&lt;/td>
 &lt;td>持續跑、不只在需要時&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="盤點工具">盤點工具&lt;/h3>
&lt;p>DevTools Performance 面板錄一段使用者操作、看 callback 觸發次數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 在 callback 內加 console.count
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mutations&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="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;mutation observer fired&amp;#39;&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="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(...);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">ResizeObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">entries&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;resize observer fired&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(...);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跑一次「使用者打字 + 等結果」的完整操作、看 console 各 listener 觸發幾次。&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-3 次&lt;/td>
 &lt;td>正常&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5-10 次&lt;/td>
 &lt;td>可能過頻、值得查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10+ 次&lt;/td>
 &lt;td>範圍 / option 太寬、需要收斂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>持續觸發（不停）&lt;/td>
 &lt;td>自激迴圈、需要立刻處理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="resizeobserver-寫變數造成自激">ResizeObserver 寫變數造成自激&lt;/h2>
&lt;p>ResizeObserver 的特殊風險是「寫 CSS 變數可能影響被觀察元素自己的尺寸」 — 這個 case 跟 &lt;a href="../mutation-observer-scope/">#29&lt;/a> 處理的 MutationObserver self-mutation 機制不同、值得獨立展開。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>監聽器的「觸發頻率」是效能的第一道防線、跨多種 listener 類型一起盤點。</strong> 本篇是 audit 視角（「我有效能問題、reactive 監聽器是不是嫌疑」）— 設計新 observer 的細節由 <a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率</a> 處理。Audit 時把所有 reactive 監聽器列一張表、看哪些觸發頻率異常。</p>
<blockquote>
<p>本篇焦點：<strong>跨 listener 類型的效能盤點</strong>。</p>
<ul>
<li><strong>MutationObserver 的設計細節</strong>（root / option / debounce / self-mutation）由 <a href="../mutation-observer-scope/">#29</a> 處理</li>
<li><strong>Selector 範圍的設計</strong>由 <a href="../dom-selector-precision/">#14</a> 處理</li>
<li><strong>Runtime 計算成本</strong>（regex / textContent / forEach）由 <a href="../runtime-iteration-and-regex-cost/">#34</a> 處理</li>
</ul></blockquote>
<hr>
<h2 id="為什麼觸發頻率主導效能">為什麼觸發頻率主導效能</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Reactive 監聽器有三個獨立成本：</p>
<table>
  <thead>
      <tr>
          <th>成本來源</th>
          <th>單次量級</th>
          <th>累積方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發頻率</td>
          <td>看範圍與 option</td>
          <td>倍數疊加</td>
      </tr>
      <tr>
          <td>Callback 內部運算</td>
          <td>看實作</td>
          <td>每次完整跑</td>
      </tr>
      <tr>
          <td>Callback 引發的副作用</td>
          <td>看 DOM 變動</td>
          <td>可能反向觸發</td>
      </tr>
  </tbody>
</table>
<p>把單次 callback 從 5ms 優化到 2ms 是 2.5x；把觸發次數從 100 次/秒降到 10 次/秒是 10x。<strong>觸發頻率優化的天花板更高</strong> — audit 時優先看頻率。</p>
<h3 id="三類觸發頻率風險速覽">三類觸發頻率風險（速覽）</h3>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>表現</th>
          <th>詳細處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範圍過寬（observer subtree）</td>
          <td>無關變動也觸發</td>
          <td><a href="../mutation-observer-scope/">#29 root 與 option 設計</a></td>
      </tr>
      <tr>
          <td>Option 全勾</td>
          <td>多種變動類型同時觸發</td>
          <td><a href="../mutation-observer-scope/">#29 三維度收斂</a></td>
      </tr>
      <tr>
          <td>自激迴圈</td>
          <td>callback 自己改 DOM 觸發自己</td>
          <td><a href="../mutation-observer-scope/">#29 self-mutation 處理</a></td>
      </tr>
  </tbody>
</table>
<p>本篇不展開設計細節（避免跟 #29 重複）、只談「audit 時怎麼識別這些 risk」。</p>
<hr>
<h2 id="跨-observer-類型的盤點">跨 observer 類型的盤點</h2>
<p>效能 audit 時、列出<strong>所有</strong> reactive 監聽器、不只 MutationObserver。各類型觸發來源不同、需要分別評估。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>觸發來源</th>
          <th>過頻訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MutationObserver</td>
          <td>DOM 變動</td>
          <td>一次操作觸發 10+ 次</td>
      </tr>
      <tr>
          <td>ResizeObserver</td>
          <td>元素尺寸變動</td>
          <td>持續觸發（自激）/ resize 視窗時連發</td>
      </tr>
      <tr>
          <td>IntersectionObserver</td>
          <td>可視性變動</td>
          <td>scroll 時連發</td>
      </tr>
      <tr>
          <td>Event listener (input / scroll / resize)</td>
          <td>使用者互動</td>
          <td>高頻事件未 debounce</td>
      </tr>
      <tr>
          <td><code>setInterval</code> / <code>requestAnimationFrame</code> 迴圈</td>
          <td>時間</td>
          <td>持續跑、不只在需要時</td>
      </tr>
  </tbody>
</table>
<h3 id="盤點工具">盤點工具</h3>
<p>DevTools Performance 面板錄一段使用者操作、看 callback 觸發次數：</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">// 在 callback 內加 console.count
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">mutations</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">count</span><span class="p">(</span><span class="s1">&#39;mutation observer fired&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="p">}).</span><span class="nx">observe</span><span class="p">(...);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">entries</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">count</span><span class="p">(</span><span class="s1">&#39;resize observer fired&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">}).</span><span class="nx">observe</span><span class="p">(...);</span></span></span></code></pre></div><p>跑一次「使用者打字 + 等結果」的完整操作、看 console 各 listener 觸發幾次。</p>
<table>
  <thead>
      <tr>
          <th>觸發次數</th>
          <th>評估</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-3 次</td>
          <td>正常</td>
      </tr>
      <tr>
          <td>5-10 次</td>
          <td>可能過頻、值得查</td>
      </tr>
      <tr>
          <td>10+ 次</td>
          <td>範圍 / option 太寬、需要收斂</td>
      </tr>
      <tr>
          <td>持續觸發（不停）</td>
          <td>自激迴圈、需要立刻處理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="resizeobserver-寫變數造成自激">ResizeObserver 寫變數造成自激</h2>
<p>ResizeObserver 的特殊風險是「寫 CSS 變數可能影響被觀察元素自己的尺寸」 — 這個 case 跟 <a href="../mutation-observer-scope/">#29</a> 處理的 MutationObserver self-mutation 機制不同、值得獨立展開。</p>
<h3 id="機制">機制</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">syncScopeHeight</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s1">&#39;--search-scope-h&#39;</span><span class="p">,</span> <span class="nx">scopeEl</span><span class="p">.</span><span class="nx">offsetHeight</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <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="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">syncScopeHeight</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</span></span></span></code></pre></div><p>如果 <code>--search-scope-h</code> 在 CSS 中被用來計算 <code>scopeEl</code> 自己的 padding / margin / height — 寫入觸發 layout、layout 觸發 resize、resize 觸發 callback、callback 又寫入。</p>
<h3 id="症狀">症狀</h3>
<ul>
<li>CPU 持續被佔</li>
<li>Performance 面板看到 ResizeObserver callback 連發（&gt;60/秒）</li>
<li>元素尺寸持續微調</li>
</ul>
<h3 id="解法">解法</h3>
<p><strong>結構分離</strong>：寫的變數不該影響被觀察元素自己。</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="k">new</span> <span class="nx">ResizeObserver</span><span class="p">(</span><span class="nx">syncScopeHeight</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">scopeEl</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// scopeEl 高度寫到 --search-scope-h
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// CSS 中 --search-scope-h 用來計算 drawer 的 margin-top
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// drawer 不是 scopeEl、不會反向觸發
</span></span></span></code></pre></div><p>設計時讓「觀察的元素」跟「受變數影響的元素」結構上分離 — 不會循環。</p>
<h3 id="跟-mutationobserver-self-mutation-的差異">跟 MutationObserver self-mutation 的差異</h3>
<table>
  <thead>
      <tr>
          <th>觀察類型</th>
          <th>self-mutation 機制</th>
          <th>處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MutationObserver</td>
          <td>callback 改 DOM 結構 / attribute</td>
          <td>disconnect + observe 配對</td>
      </tr>
      <tr>
          <td>ResizeObserver</td>
          <td>callback 改變數 → 反向影響尺寸</td>
          <td>結構分離（觀察 A、影響 B）</td>
      </tr>
      <tr>
          <td>IntersectionObserver</td>
          <td>callback 改可視性 → 反向觸發</td>
          <td>罕見、設計時避免</td>
      </tr>
  </tbody>
</table>
<p>ResizeObserver 沒有 disconnect 配對的等價技巧（disconnect 後再 observe 仍會立即重觸發） — 必須靠結構分離。</p>
<hr>
<h2 id="盤點的標準格式">盤點的標準格式</h2>
<p>每個 reactive 監聽器寫成一段註解、audit 時讀這份「設定卡」即可：</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="cm">/**
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cm"> * 監聽：.pagefind-ui 的子節點變動
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="cm"> * 類型：MutationObserver
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="cm"> * 範圍：subtree（深層也看）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="cm"> * Option：childList only
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="cm"> * Callback 是否改 DOM：是（toggle class）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="cm"> * 自激風險：否（class change 不觸發 childList）
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="cm"> * Debounce：80ms
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="cm"> * 預期觸發頻率：使用者打字一次 &lt; 5 次
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">schedule</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">ui</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>audit 時、看註解就知道：</p>
<ul>
<li>這個 observer 在做什麼</li>
<li>預期觸發頻率多少</li>
<li>實測超過預期 → 範圍太寬或 option 過勾</li>
</ul>
<hr>
<h2 id="設計取捨頻率管理策略選擇">設計取捨：頻率管理策略選擇</h2>
<p>當盤點發現某個 observer 觸發過頻、四種應對：</p>
<h3 id="a縮-observer-範圍--option這個專案的預設">A：縮 observer 範圍 / option（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：subtree → 直接子；移除沒用的 option flag</li>
<li><strong>選 A 的理由</strong>：成本最低、改一行；觸發頻率倍數降低</li>
<li><strong>適合</strong>：絕大多數過頻 case</li>
<li><strong>代價</strong>：需要重新確認哪些變動類型真的需要監聽</li>
<li><strong>詳細</strong>：<a href="../mutation-observer-scope/">#29 三維度收斂</a></li>
</ul>
<h3 id="b加-debounce--throttle">B：加 debounce / throttle</h3>
<ul>
<li><strong>機制</strong>：高頻觸發合併成低頻 apply</li>
<li><strong>跟 A 的取捨</strong>：B 不解問題的根（觸發仍發生）、A 解根；但 B 對「無法縮範圍」的 case（如 input event）必要</li>
<li><strong>B 比 A 好的情境</strong>：使用者輸入事件、scroll 事件 — 本身高頻、無法縮範圍</li>
</ul>
<h3 id="cdisconnect--reconnect-配對">C：Disconnect / reconnect 配對</h3>
<ul>
<li><strong>機制</strong>：callback 改 DOM 前 disconnect、改完 reconnect</li>
<li><strong>跟 A/B 的取捨</strong>：C 處理 self-mutation、A/B 不處理；C 比 A/B 複雜</li>
<li><strong>C 比 A/B 好的情境</strong>：MutationObserver callback 必須改 DOM（沒有結構分離選項）</li>
<li><strong>詳細</strong>：<a href="../mutation-observer-scope/">#29 self-mutation 處理</a></li>
</ul>
<h3 id="dresizeobserver-結構分離">D：ResizeObserver 結構分離</h3>
<ul>
<li><strong>機制</strong>：觀察 A、影響 B（B ≠ A）</li>
<li><strong>跟 C 的取捨</strong>：ResizeObserver 沒 disconnect 等價技巧、必須用 D</li>
<li><strong>D 是 ResizeObserver 自激的唯一解</strong></li>
</ul>
<hr>
<h2 id="不該套用頻率管理的情境">不該套用「頻率管理」的情境</h2>
<p>不是所有 reactive 監聽器都需要管：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼可以放任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開發階段、不上 production</td>
          <td>效能不影響真實使用者</td>
      </tr>
      <tr>
          <td>Callback 極輕（單次 &lt; 0.1ms）</td>
          <td>觸發 100 次也才 10ms</td>
      </tr>
      <tr>
          <td>觸發頻率本來就極低（一次 setup 一次 callback）</td>
          <td>沒有頻率問題</td>
      </tr>
  </tbody>
</table>
<p><strong>核心判準</strong>：實測有效能問題嗎？沒有就不必預先優化。Audit 是「找已存在的問題」、不是「預防所有可能」。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率</a></td>
          <td>互補 — #29 是設計指引（怎麼寫 observer）、本篇是 audit 視角（怎麼找問題）</td>
      </tr>
      <tr>
          <td><a href="../dom-selector-precision/">#14 Selector 精準度</a></td>
          <td>跟 observer 範圍同源 — selector 起點就是 observer root 的選擇基礎</td>
      </tr>
      <tr>
          <td><a href="../runtime-iteration-and-regex-cost/">#34 Runtime 計算成本</a></td>
          <td>互補 — 本篇看「觸發次數」、#34 看「單次 callback 成本」</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>「縮監聽範圍」是「最小必要範圍」原則的應用</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者操作後瀏覽器卡頓</td>
          <td>該操作觸發了哪些 observer、各自觸發次數</td>
      </tr>
      <tr>
          <td>CPU 持續 100%</td>
          <td>observer 自激迴圈（特別是 ResizeObserver）</td>
      </tr>
      <tr>
          <td><code>setTimeout(0)</code> 也來不及處理</td>
          <td>observer / event 觸發頻率超過 schedule 處理速度</td>
      </tr>
      <tr>
          <td>Callback 內加 console.count 數字爆炸</td>
          <td>observer 範圍過寬 — 收斂方式由 <a href="../mutation-observer-scope/">#29</a> 處理</td>
      </tr>
      <tr>
          <td>ResizeObserver 在某 callback 後持續觸發</td>
          <td>寫的變數反向影響觀察元素 — 結構分離</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：reactive 監聽器的效能 audit = 列所有 listener + 量觸發次數 + 比對預期。發現問題後、設計修正方式由 <a href="../mutation-observer-scope/">#29</a> 等設計指引篇展開 — 本篇只負責「找問題」這一步。</p>
]]></content:encoded></item><item><title>Runtime 計算成本：每筆迭代與正則</title><link>https://tarrragon.github.io/blog/report/runtime-iteration-and-regex-cost/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/runtime-iteration-and-regex-cost/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>每筆迭代的成本 = 單次計算 × 迭代次數。&lt;/strong> 兩個變數都會放大效能問題；單次計算便宜時、迭代次數變多仍可能爆掉 frame budget。盤點時兩維度一起看、不只看單筆。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼迭代次數值得獨立看待">為什麼迭代次數值得獨立看待&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>開發階段測試的資料量通常少（10 筆結果）— 單次迭代 + 10 次 = 不痛。&lt;/p>
&lt;p>上線後資料量放大（200 筆結果）— 同樣的單次計算 × 200 = 痛。&lt;/p>
&lt;p>&lt;strong>單次計算的最佳化收益是固定倍數、迭代次數的成長是線性放大&lt;/strong> — 後者更值得關注。&lt;/p>
&lt;h3 id="三類迭代成本">三類迭代成本&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>對 DOM 集合迭代&lt;/td>
 &lt;td>&lt;code>forEach&lt;/code> over &lt;code>querySelectorAll&lt;/code> 結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對資料陣列迭代&lt;/td>
 &lt;td>&lt;code>map&lt;/code> / &lt;code>filter&lt;/code> over 大量物件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對 DOM 樹遞迴&lt;/td>
 &lt;td>&lt;code>.contains()&lt;/code> 或 ancestor walk&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每類有不同的優化策略、共通是「先量規模再決定動哪」。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1scope-filter-對每筆-result-跑-regex">風險 1：scope filter 對每筆 result 跑 regex&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;code>assets/search.js&lt;/code> 的 &lt;code>apply()&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">titleEl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result-title&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">excerptEl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result-excerpt&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">title&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">titleEl&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">titleEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">excerpt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">excerptEl&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">excerptEl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">show&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">title&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="nx">re&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">excerpt&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每筆 result 做的事：&lt;/p>
&lt;ol>
&lt;li>兩次 &lt;code>querySelector&lt;/code>（DOM 查詢）&lt;/li>
&lt;li>兩次 &lt;code>textContent&lt;/code> 讀取（DOM 屬性讀取）&lt;/li>
&lt;li>一次 &lt;code>re.test&lt;/code>（正則比對）&lt;/li>
&lt;li>一次 &lt;code>classList.toggle&lt;/code>（class 操作）&lt;/li>
&lt;/ol>
&lt;p>單筆 ~0.1ms 等級、看 DOM 大小。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>結果 10 筆 → 1ms、無感&lt;/li>
&lt;li>結果 100 筆 → 10ms、接近 frame budget（16.67ms）&lt;/li>
&lt;li>結果 500 筆 → 50ms、明顯卡頓&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者打字時 input lag、scroll jank。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：DevTools Performance 面板錄一次 apply、看 forEach 那段佔多少。&amp;gt; 5ms 開始考慮優化。&lt;/p>
&lt;h3 id="風險-2textcontent-讀取的隱藏成本">風險 2：textContent 讀取的隱藏成本&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：上述 &lt;code>titleEl.textContent&lt;/code>。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;code>textContent&lt;/code> 看似簡單、實際在某些瀏覽器中要 traverse 整個子樹拼字串。對於有 highlight &lt;code>&amp;lt;mark&amp;gt;&lt;/code> 標籤的結果、textContent 要組合多個 text node。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：textContent 比預期慢、特別在 result 內結構複雜時。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：用 &lt;code>console.time&lt;/code> 量一次 textContent 讀取、看單次幾 ms。&lt;/p>
&lt;h3 id="風險-3每次-apply-都重新-queryselector">風險 3：每次 apply 都重新 querySelector&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;code>apply()&lt;/code> 每次跑都 &lt;code>document.querySelectorAll('.pagefind-ui__result')&lt;/code>。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：querySelector 是 fresh 查詢、不快取。每次 apply 都重新掃 DOM 找到結果集合。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：apply 觸發頻繁時、querySelector 是固定開銷。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：把結果集合 cache 一份、observer 觸發時更新 cache、apply 用 cache 不重查 DOM。&lt;/p>
&lt;h3 id="風險-4regex-編譯成本">風險 4：Regex 編譯成本&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">re&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nb">RegExp&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">escapeRegex&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="s1">&amp;#39;i&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每次 apply 編譯一次 regex。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：Regex 編譯成本比想像中重 — 對複雜 pattern 可達數 ms。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：query 字串長、apply 觸發頻繁時、regex 編譯佔 frame budget。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：把 regex cache 起來、query 變動才重編譯。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>每筆迭代的成本 = 單次計算 × 迭代次數。</strong> 兩個變數都會放大效能問題；單次計算便宜時、迭代次數變多仍可能爆掉 frame budget。盤點時兩維度一起看、不只看單筆。</p>
<hr>
<h2 id="為什麼迭代次數值得獨立看待">為什麼迭代次數值得獨立看待</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>開發階段測試的資料量通常少（10 筆結果）— 單次迭代 + 10 次 = 不痛。</p>
<p>上線後資料量放大（200 筆結果）— 同樣的單次計算 × 200 = 痛。</p>
<p><strong>單次計算的最佳化收益是固定倍數、迭代次數的成長是線性放大</strong> — 後者更值得關注。</p>
<h3 id="三類迭代成本">三類迭代成本</h3>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對 DOM 集合迭代</td>
          <td><code>forEach</code> over <code>querySelectorAll</code> 結果</td>
      </tr>
      <tr>
          <td>對資料陣列迭代</td>
          <td><code>map</code> / <code>filter</code> over 大量物件</td>
      </tr>
      <tr>
          <td>對 DOM 樹遞迴</td>
          <td><code>.contains()</code> 或 ancestor walk</td>
      </tr>
  </tbody>
</table>
<p>每類有不同的優化策略、共通是「先量規模再決定動哪」。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1scope-filter-對每筆-result-跑-regex">風險 1：scope filter 對每筆 result 跑 regex</h3>
<p><strong>位置</strong>：<code>assets/search.js</code> 的 <code>apply()</code>。</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="nx">items</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">titleEl</span>   <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result-title&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">var</span> <span class="nx">excerptEl</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result-excerpt&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">var</span> <span class="nx">title</span>   <span class="o">=</span> <span class="nx">titleEl</span>   <span class="o">?</span> <span class="nx">titleEl</span><span class="p">.</span><span class="nx">textContent</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="kd">var</span> <span class="nx">excerpt</span> <span class="o">=</span> <span class="nx">excerptEl</span> <span class="o">?</span> <span class="nx">excerptEl</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">:</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kd">var</span> <span class="nx">show</span> <span class="o">=</span> <span class="nx">scope</span> <span class="o">===</span> <span class="s1">&#39;title&#39;</span> <span class="o">?</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="o">:</span> <span class="nx">re</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">excerpt</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>每筆 result 做的事：</p>
<ol>
<li>兩次 <code>querySelector</code>（DOM 查詢）</li>
<li>兩次 <code>textContent</code> 讀取（DOM 屬性讀取）</li>
<li>一次 <code>re.test</code>（正則比對）</li>
<li>一次 <code>classList.toggle</code>（class 操作）</li>
</ol>
<p>單筆 ~0.1ms 等級、看 DOM 大小。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>結果 10 筆 → 1ms、無感</li>
<li>結果 100 筆 → 10ms、接近 frame budget（16.67ms）</li>
<li>結果 500 筆 → 50ms、明顯卡頓</li>
</ul>
<p><strong>症狀</strong>：使用者打字時 input lag、scroll jank。</p>
<p><strong>第一個該查的</strong>：DevTools Performance 面板錄一次 apply、看 forEach 那段佔多少。&gt; 5ms 開始考慮優化。</p>
<h3 id="風險-2textcontent-讀取的隱藏成本">風險 2：textContent 讀取的隱藏成本</h3>
<p><strong>位置</strong>：上述 <code>titleEl.textContent</code>。</p>
<p><strong>判讀</strong>：<code>textContent</code> 看似簡單、實際在某些瀏覽器中要 traverse 整個子樹拼字串。對於有 highlight <code>&lt;mark&gt;</code> 標籤的結果、textContent 要組合多個 text node。</p>
<p><strong>症狀</strong>：textContent 比預期慢、特別在 result 內結構複雜時。</p>
<p><strong>第一個該查的</strong>：用 <code>console.time</code> 量一次 textContent 讀取、看單次幾 ms。</p>
<h3 id="風險-3每次-apply-都重新-queryselector">風險 3：每次 apply 都重新 querySelector</h3>
<p><strong>位置</strong>：<code>apply()</code> 每次跑都 <code>document.querySelectorAll('.pagefind-ui__result')</code>。</p>
<p><strong>判讀</strong>：querySelector 是 fresh 查詢、不快取。每次 apply 都重新掃 DOM 找到結果集合。</p>
<p><strong>症狀</strong>：apply 觸發頻繁時、querySelector 是固定開銷。</p>
<p><strong>第一個該查的</strong>：把結果集合 cache 一份、observer 觸發時更新 cache、apply 用 cache 不重查 DOM。</p>
<h3 id="風險-4regex-編譯成本">風險 4：Regex 編譯成本</h3>
<p><strong>位置</strong>：</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="kd">var</span> <span class="nx">re</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="nx">escapeRegex</span><span class="p">(</span><span class="nx">query</span><span class="p">),</span> <span class="s1">&#39;i&#39;</span><span class="p">);</span></span></span></code></pre></div><p>每次 apply 編譯一次 regex。</p>
<p><strong>判讀</strong>：Regex 編譯成本比想像中重 — 對複雜 pattern 可達數 ms。</p>
<p><strong>症狀</strong>：query 字串長、apply 觸發頻繁時、regex 編譯佔 frame budget。</p>
<p><strong>第一個該查的</strong>：把 regex cache 起來、query 變動才重編譯。</p>
<hr>
<h2 id="內在屬性比較四種優化方向">內在屬性比較：四種優化方向</h2>
<table>
  <thead>
      <tr>
          <th>方向</th>
          <th>縮減幅度</th>
          <th>複雜度</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>縮迭代次數（IntersectionObserver 只處理可視區）</td>
          <td>大</td>
          <td>中</td>
          <td>結果數量大、多數不在可視範圍</td>
      </tr>
      <tr>
          <td>縮單次計算（cache textContent / regex）</td>
          <td>中</td>
          <td>低</td>
          <td>重複計算同樣的東西</td>
      </tr>
      <tr>
          <td>分批處理（requestIdleCallback / chunk）</td>
          <td>大 — 攤開時間</td>
          <td>中</td>
          <td>一次處理量大但可延後</td>
      </tr>
      <tr>
          <td>Web Worker</td>
          <td>最大 — 獨立 thread</td>
          <td>高</td>
          <td>純計算密集、跟 DOM 無關</td>
      </tr>
  </tbody>
</table>
<p>對 scope filter 的場景：<strong>IntersectionObserver 只處理可視區</strong> + <strong>regex cache</strong> 是性價比最高的兩項。</p>
<hr>
<h2 id="規模放大的盤點">規模放大的盤點</h2>
<p>對每個迭代的 callback、預先估算「規模放大時會怎樣」：</p>
<table>
  <thead>
      <tr>
          <th>當前規模</th>
          <th>10x 規模</th>
          <th>100x 規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 筆 result × 0.1ms = 1ms</td>
          <td>100 筆 = 10ms（接近 16ms 上限）</td>
          <td>1000 筆 = 100ms（明顯卡）</td>
      </tr>
  </tbody>
</table>
<p>10x / 100x 的數字是「未來內容增長 1 個 / 2 個數量級」的預警。當前 fine 但 10x 後不 fine、值得提前考慮優化機制。</p>
<hr>
<h2 id="設計取捨per-item-迭代成本的優化策略">設計取捨：per-item 迭代成本的優化策略</h2>
<p>四種做法、各自機會成本不同。預設先做 A（縮迭代次數）、A 不夠才考慮 B/C/D。</p>
<h3 id="a縮迭代次數intersectionobserver--分頁--過濾這個專案的預設">A：縮迭代次數（IntersectionObserver / 分頁 / 過濾）（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：用 IntersectionObserver 只處理可視區、用過濾條件預先排除大量項目</li>
<li><strong>選 A 的理由</strong>：縮減幅度大（線性放大反向操作）、callback 內部不變</li>
<li><strong>適合</strong>：結果數量大、但實際需要處理的部分少（多數在可視區外）</li>
<li><strong>代價</strong>：增加 observer setup、需要設計「該處理什麼項目」的判斷</li>
</ul>
<h3 id="b縮單次計算cache-textcontent--regex--dom-query">B：縮單次計算（cache textContent / regex / DOM query）</h3>
<ul>
<li><strong>機制</strong>：把重複計算的結果 cache、避免每次重做</li>
<li><strong>跟 A 的取捨</strong>：B 縮減幅度中等（看 cache 命中率）、A 縮減幅度大；兩者解不同問題、可並用</li>
<li><strong>B 比 A 好的情境</strong>：迭代次數無法縮（必須處理所有項目）、但每項計算重複（regex 編譯、textContent 重讀）</li>
</ul>
<h3 id="c分批處理requestidlecallback--chunk">C：分批處理（requestIdleCallback / chunk）</h3>
<ul>
<li><strong>機制</strong>：把一次處理拆成多次、攤開到多個 frame</li>
<li><strong>跟 A/B 的取捨</strong>：C 攤開時間、A/B 縮減總時間；C 在「總時間無法縮、但可以延後」時合理</li>
<li><strong>C 比 A 好的情境</strong>：處理量大但可延後（initial render 時的非關鍵 enhancement）</li>
</ul>
<h3 id="dweb-worker">D：Web Worker</h3>
<ul>
<li><strong>機制</strong>：把計算搬到獨立 thread</li>
<li><strong>跟 A/B/C 的取捨</strong>：D 完全不阻 main thread、但 setup 成本高（postMessage 序列化）</li>
<li><strong>D 才合理的情境</strong>：純計算密集、跟 DOM 無關（搜尋 indexing、複雜資料處理）— 對 DOM 操作沒意義（Web Worker 不能直接動 DOM）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>forEach over 大集合佔用 frame budget</td>
          <td>用 IntersectionObserver 只處理可視區</td>
      </tr>
      <tr>
          <td>每次 apply 重做相同的查詢 / 編譯</td>
          <td>Cache 結果、變動觸發時更新 cache</td>
      </tr>
      <tr>
          <td>Async 處理可接受時還在同步跑</td>
          <td>改 requestIdleCallback / 分批 setTimeout</td>
      </tr>
      <tr>
          <td>資料量比測試時大 N 倍後才發現問題</td>
          <td>開發時做規模 10x / 100x 預估</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：「每筆都做」的計算成本 = 每筆 × 筆數。優化時兩維度都看、不要只盯單次。</p>
]]></content:encoded></item><item><title>Layout reflow / repaint 的可量化評估</title><link>https://tarrragon.github.io/blog/report/layout-reflow-measurement/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/layout-reflow-measurement/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Reflow 與 repaint 的成本差兩個數量級、用 Performance 面板可以量化判斷哪個發生。&lt;/strong> 開發時不需要「全部避開 reflow」、要做的是「知道哪些操作觸發 reflow、規模放大時哪些值得優化」。&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>Style recalc&lt;/td>
 &lt;td>CSS 規則變動、class toggle&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Layout (reflow)&lt;/td>
 &lt;td>影響元素尺寸 / 位置的 CSS 改變&lt;/td>
 &lt;td>高（要重算所有受影響元素）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paint (repaint)&lt;/td>
 &lt;td>顏色 / 背景變動但位置不變&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Composite&lt;/td>
 &lt;td>transform / opacity 等 GPU 加速屬性&lt;/td>
 &lt;td>最低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不同操作落在不同階段。「改 width」觸發 reflow、「改 transform」只到 composite。差距 ~10-100x。&lt;/p>
&lt;p>但這不代表要「永遠用 transform」 — 多數場景 reflow 成本可以接受、過度避免反而讓 layout 變脆。&lt;/p>
&lt;h3 id="量化的工具">量化的工具&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>看什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Chrome DevTools Performance&lt;/td>
 &lt;td>整段操作的 reflow / paint / composite 時間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Performance API（&lt;code>performance.measure&lt;/code>）&lt;/td>
 &lt;td>程式化量自家函式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Layout shift (Web Vitals CLS)&lt;/td>
 &lt;td>視覺上的 layout 跳動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優先用 DevTools Performance 量、有具體數字後再決定是否優化。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1filter-slot-跨-viewport-切換">風險 1：Filter slot 跨 viewport 切換&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：matchMedia callback 內 &lt;code>slot.appendChild(filter)&lt;/code> / &lt;code>drawer.insertBefore(filter, ...)&lt;/code>。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>整個 filter 子樹移動 = layout 重算（filter 的新位置、原位置元素重排）&lt;/li>
&lt;li>同時 main 區域與 sidebar 區域的尺寸都重算&lt;/li>
&lt;li>一次性發生、不持續觸發&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者拖動視窗寬度跨過 1400px 時、瞬間卡頓 1-2 frame。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：DevTools Performance 錄下 resize 跨過 breakpoint 的瞬間、看 Layout 區塊有多大。&amp;lt; 16ms = OK；&amp;gt; 16ms 考慮 debounce matchMedia callback。&lt;/p>
&lt;h3 id="風險-2css-變數寫入">風險 2：CSS 變數寫入&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;code>document.body.style.setProperty('--search-scope-h', ...)&lt;/code>。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：寫 CSS 變數不一定觸發 reflow — 看哪些規則用了這個變數、那些規則影響哪些元素。&lt;/p>
&lt;ul>
&lt;li>&lt;code>--search-scope-h&lt;/code> 用於 drawer 的 &lt;code>margin-top&lt;/code> → drawer 位置變動 → reflow&lt;/li>
&lt;li>&lt;code>--search-scope-h&lt;/code> 用於 filter slot 的 &lt;code>padding-top&lt;/code> → filter slot 高度變動 → reflow&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：scope 大小變動時、drawer 與 filter slot 同時重排、可能看到輕微跳動。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：DevTools Performance 錄一次 scope 變大的事件、看 Layout 區塊。多數場景 &amp;lt; 5ms、可忽略。&lt;/p>
&lt;h3 id="風險-3absolute-定位的重算">風險 3：Absolute 定位的重算&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;code>.search-filter-slot { position: absolute; ... }&lt;/code>。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：Absolute 元素跟一般 flow 元素分離、自己不影響 sibling 的 layout、但仍受自身 position / size 變動影響。Filter 改 top 觸發自身 reflow、不影響 main。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Reflow 與 repaint 的成本差兩個數量級、用 Performance 面板可以量化判斷哪個發生。</strong> 開發時不需要「全部避開 reflow」、要做的是「知道哪些操作觸發 reflow、規模放大時哪些值得優化」。</p>
<hr>
<h2 id="為什麼要量化不憑感覺">為什麼要量化、不憑感覺</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>瀏覽器渲染管線分階段：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>觸發條件</th>
          <th>相對成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Style recalc</td>
          <td>CSS 規則變動、class toggle</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Layout (reflow)</td>
          <td>影響元素尺寸 / 位置的 CSS 改變</td>
          <td>高（要重算所有受影響元素）</td>
      </tr>
      <tr>
          <td>Paint (repaint)</td>
          <td>顏色 / 背景變動但位置不變</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Composite</td>
          <td>transform / opacity 等 GPU 加速屬性</td>
          <td>最低</td>
      </tr>
  </tbody>
</table>
<p>不同操作落在不同階段。「改 width」觸發 reflow、「改 transform」只到 composite。差距 ~10-100x。</p>
<p>但這不代表要「永遠用 transform」 — 多數場景 reflow 成本可以接受、過度避免反而讓 layout 變脆。</p>
<h3 id="量化的工具">量化的工具</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>看什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Chrome DevTools Performance</td>
          <td>整段操作的 reflow / paint / composite 時間</td>
      </tr>
      <tr>
          <td>Performance API（<code>performance.measure</code>）</td>
          <td>程式化量自家函式</td>
      </tr>
      <tr>
          <td>Layout shift (Web Vitals CLS)</td>
          <td>視覺上的 layout 跳動</td>
      </tr>
  </tbody>
</table>
<p>優先用 DevTools Performance 量、有具體數字後再決定是否優化。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1filter-slot-跨-viewport-切換">風險 1：Filter slot 跨 viewport 切換</h3>
<p><strong>位置</strong>：matchMedia callback 內 <code>slot.appendChild(filter)</code> / <code>drawer.insertBefore(filter, ...)</code>。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>整個 filter 子樹移動 = layout 重算（filter 的新位置、原位置元素重排）</li>
<li>同時 main 區域與 sidebar 區域的尺寸都重算</li>
<li>一次性發生、不持續觸發</li>
</ul>
<p><strong>症狀</strong>：使用者拖動視窗寬度跨過 1400px 時、瞬間卡頓 1-2 frame。</p>
<p><strong>第一個該查的</strong>：DevTools Performance 錄下 resize 跨過 breakpoint 的瞬間、看 Layout 區塊有多大。&lt; 16ms = OK；&gt; 16ms 考慮 debounce matchMedia callback。</p>
<h3 id="風險-2css-變數寫入">風險 2：CSS 變數寫入</h3>
<p><strong>位置</strong>：<code>document.body.style.setProperty('--search-scope-h', ...)</code>。</p>
<p><strong>判讀</strong>：寫 CSS 變數不一定觸發 reflow — 看哪些規則用了這個變數、那些規則影響哪些元素。</p>
<ul>
<li><code>--search-scope-h</code> 用於 drawer 的 <code>margin-top</code> → drawer 位置變動 → reflow</li>
<li><code>--search-scope-h</code> 用於 filter slot 的 <code>padding-top</code> → filter slot 高度變動 → reflow</li>
</ul>
<p><strong>症狀</strong>：scope 大小變動時、drawer 與 filter slot 同時重排、可能看到輕微跳動。</p>
<p><strong>第一個該查的</strong>：DevTools Performance 錄一次 scope 變大的事件、看 Layout 區塊。多數場景 &lt; 5ms、可忽略。</p>
<h3 id="風險-3absolute-定位的重算">風險 3：Absolute 定位的重算</h3>
<p><strong>位置</strong>：<code>.search-filter-slot { position: absolute; ... }</code>。</p>
<p><strong>判讀</strong>：Absolute 元素跟一般 flow 元素分離、自己不影響 sibling 的 layout、但仍受自身 position / size 變動影響。Filter 改 top 觸發自身 reflow、不影響 main。</p>
<p><strong>症狀</strong>：filter slot 的 padding-top 變動（隨 scope-h）— 只影響 filter 自身高度。</p>
<p><strong>第一個該查的</strong>：DevTools Performance 看 filter padding 變動時的 layout 範圍 — 應該只到 filter 內部、不擴散到 main / footer。若擴散表示有意外的 stacking context 影響。</p>
<h3 id="風險-4js-連續操作-dom">風險 4：JS 連續操作 DOM</h3>
<p><strong>位置</strong>：<code>reorderFilters()</code> 用 <code>appendChild</code> 多次調整順序。</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="nx">desiredOrder</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">])</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">byKey</span><span class="p">[</span><span class="nx">k</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p><strong>判讀</strong>：</p>
<ul>
<li>多次 <code>appendChild</code> 可能觸發多次 layout</li>
<li>但 browser 通常會合併同步 DOM 變動到一次 layout（natural batching）</li>
<li>真正會「強制 layout」的是 DOM 寫入後馬上讀 layout 屬性（如 offsetHeight）</li>
</ul>
<p><strong>症狀</strong>：rare — reorder 一次只在 setup 時跑、影響很短。</p>
<p><strong>第一個該查的</strong>：若有這類「寫後立刻讀」的 pattern、用 <code>requestAnimationFrame</code> 把讀延後到下一幀、避免 forced sync layout。</p>
<hr>
<h2 id="內在屬性比較四種-layout-變動類型">內在屬性比較：四種 layout 變動類型</h2>
<table>
  <thead>
      <tr>
          <th>變動類型</th>
          <th>成本</th>
          <th>可控性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Composite-only（transform / opacity）</td>
          <td>最低</td>
          <td>GPU 加速、&lt; 1ms</td>
      </tr>
      <tr>
          <td>Paint-only（顏色變動）</td>
          <td>低</td>
          <td>局部重繪</td>
      </tr>
      <tr>
          <td>Layout（尺寸 / 位置變動）</td>
          <td>中-高</td>
          <td>要算受影響的範圍</td>
      </tr>
      <tr>
          <td>Forced sync layout（DOM 寫後立刻讀）</td>
          <td>最高</td>
          <td>連續觸發是 perf killer</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>有意識避免 forced sync layout</strong>、<strong>對動畫優先用 transform</strong>、<strong>一般 layout 變動量小不必特別避免</strong>。</p>
<hr>
<h2 id="預估成本的快速法則">預估成本的快速法則</h2>
<p>不要每個操作都用 DevTools 量、用快速法則先判斷：</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>預估等級</th>
          <th>何時要量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改 class（class toggle）</td>
          <td>1ms 等級</td>
          <td>套用到大量元素時</td>
      </tr>
      <tr>
          <td>Append / remove 單一節點</td>
          <td>1-5ms</td>
          <td>大規模迭代時</td>
      </tr>
      <tr>
          <td>移動 DOM 子樹（reparent）</td>
          <td>5-20ms</td>
          <td>子樹大、頻繁觸發時</td>
      </tr>
      <tr>
          <td>改 CSS 變數（簡單 calc）</td>
          <td>1-5ms</td>
          <td>頻繁觸發時</td>
      </tr>
      <tr>
          <td>Forced sync layout</td>
          <td>5-50ms</td>
          <td>任何寫後立刻讀的 pattern 都該量</td>
      </tr>
  </tbody>
</table>
<p>預估超過 frame budget（16.67ms）才值得實際量、進一步優化。</p>
<hr>
<h2 id="設計取捨layout-操作的處理策略">設計取捨：layout 操作的處理策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（量化評估再決定）當預設、其他做法在特定情境合理。</p>
<h3 id="a量化評估按規模決定優化與否這個專案的預設">A：量化評估、按規模決定優化與否（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：用 DevTools Performance 量每個 layout 操作的實際成本、超過 frame budget（16.67ms）才優化</li>
<li><strong>選 A 的理由</strong>：避免過度優化（多數 reflow 成本可接受）、又不漏真正貴的（forced sync layout）</li>
<li><strong>適合</strong>：所有效能盤點情境</li>
<li><strong>代價</strong>：需要學會用 DevTools Performance、對效能 dispute 要量</li>
</ul>
<h3 id="b全部用-transform--opacity-避免-reflow">B：全部用 transform / opacity 避免 reflow</h3>
<ul>
<li><strong>機制</strong>：所有動畫 / 變動都用 transform 或 opacity（GPU composite）</li>
<li><strong>跟 A 的取捨</strong>：B 預先避免 reflow、A 量化按需處理；但 B 寫出複雜的 transform / absolute 組合、layout 邏輯難維護</li>
<li><strong>B 比 A 好的情境</strong>：高頻動畫（每 frame 變動的旋轉 / 移動）— 確定觸發 layout 會卡</li>
</ul>
<h3 id="c完全避免-layout-操作">C：完全避免 layout 操作</h3>
<ul>
<li><strong>機制</strong>：把所有可能觸發 reflow 的操作都繞開</li>
<li><strong>跟 A/B 的取捨</strong>：C 過度反應、A/B 適度；C 寫法極受限、layout 表達力下降</li>
<li><strong>C 才合理的情境</strong>：純動畫場景（沒有 layout 需求）— 對一般 UI 不適用</li>
</ul>
<h3 id="d不量靠經驗判斷">D：不量、靠經驗判斷</h3>
<ul>
<li><strong>機制</strong>：依「我覺得這應該快」做決定</li>
<li><strong>成本特別高的原因</strong>：瀏覽器 / 設備 / 場景差異大、直覺不可靠；可能漏掉 forced sync layout 等真正貴的 pattern</li>
<li><strong>D 是反模式</strong>：效能 dispute 必須有數字 — 直覺判斷會漏掉 forced sync layout 等真正貴的 pattern、跨設備差異大</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者操作後輕微跳動或卡頓</td>
          <td>DevTools Performance 看 Layout 區塊</td>
      </tr>
      <tr>
          <td>動畫不順</td>
          <td>確認動畫屬性是 transform / opacity 而非 width / left</td>
      </tr>
      <tr>
          <td>Layout shift 警告</td>
          <td>找出觸發 layout 的元素、量穩定性</td>
      </tr>
      <tr>
          <td>Console 出現「Forced reflow」warning</td>
          <td>找寫後立刻讀的 DOM pattern</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Reflow 是 layout 系統的正常運作、不是要消滅的敵人。盤點時量化看哪些值得優化、哪些可以接受。</p>
]]></content:encoded></item><item><title>資源載入時序：lazy chunk 與 critical path</title><link>https://tarrragon.github.io/blog/report/lazy-loading-and-critical-path/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/lazy-loading-and-critical-path/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資源載入時序的設計選擇是「首次渲染速度」與「首次互動延遲」的權衡 — 不是越早載越好。&lt;/strong> 把不影響首次渲染的資源延後（lazy load）、首屏更快；但延後的資源在使用者真正需要時可能還沒到、互動延遲。盤點時兩者一起看。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>開始下載&lt;/td>
 &lt;td>在 critical path（首屏）還是 lazy（首次互動才下載）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用&lt;/td>
 &lt;td>下載完 + parse + 執行完&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把資源放 critical path = 阻塞首屏渲染；放 lazy = 首屏更快但首次互動可能等。&lt;/p>
&lt;p>對搜尋頁：使用者打開 &lt;code>/search/&lt;/code> 但可能不立刻搜尋 — pagefind index lazy load 是合理選擇。但若打開後立刻打字、index 還沒載完、第一次搜尋有明顯延遲。&lt;/p>
&lt;h3 id="critical-path-vs-lazy-的標準">Critical path vs lazy 的標準&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>視覺主體 CSS（首屏看到的）&lt;/td>
 &lt;td>Critical path&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>互動 JS（事件處理）&lt;/td>
 &lt;td>DOMContentLoaded 後即可&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大型功能模組（搜尋 index）&lt;/td>
 &lt;td>Lazy、使用者觸發才載&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>圖片 / 影片&lt;/td>
 &lt;td>Lazy 視可見性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇原則：&lt;strong>「首屏渲染需要嗎？」是 → critical；「使用者一定會用嗎？」否 → lazy&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1pagefind-index-下載延遲">風險 1：Pagefind index 下載延遲&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：PagefindUI 在 mount 時開始下載 entry chunk、之後才能搜尋。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>entry chunk（&lt;code>pagefind-entry.json&lt;/code>）~ 10KB&lt;/li>
&lt;li>下載 + parse 約 100-500ms（看網路）&lt;/li>
&lt;li>使用者打開搜尋頁立刻打字時、第一個字可能還沒搜尋&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者打開 /search/ 立刻打字、第一個字沒回應、過 200-500ms 才開始搜尋。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：DevTools Network 看 entry chunk 下載時間。&amp;gt; 500ms 考慮 preload 機制。&lt;/p>
&lt;h3 id="風險-2個別-search-chunk-的-lazy-load">風險 2：個別 search chunk 的 lazy load&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：使用者搜尋特定 term 時、pagefind 動態下載對應 chunk。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：每個搜尋 term 對應一個 chunk（依 term 前綴分）。第一次搜尋某個 prefix 要下載對應 chunk、之後同 prefix 搜尋走 cache。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：搜尋特定字時稍有延遲（200-500ms）、之後就快了。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：Pagefind 內建 cache 機制、多數情境表現可接受。若極慢可考慮 service worker preload chunk。&lt;/p>
&lt;h3 id="風險-3pagefind-ui-script-下載">風險 3：Pagefind UI script 下載&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;code>&amp;lt;script src=&amp;quot;/blog/pagefind/pagefind-ui.js&amp;quot;&amp;gt;&lt;/code>。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>~ 50KB minified、需在使用者打字前載完&lt;/li>
&lt;li>有 &lt;code>defer&lt;/code> 不阻塞 HTML parsing、但仍占 critical path 寬度&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：搜尋頁初次載入比一般頁慢。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：確認 &lt;code>&amp;lt;script&amp;gt;&lt;/code> 有 &lt;code>defer&lt;/code> attribute、使用者開啟搜尋頁後背景下載、不阻塞 HTML 渲染。&lt;/p>
&lt;h3 id="風險-4assetssearchcss-與-pagefind-uicss-載入順序">風險 4：assets/search.css 與 pagefind-ui.css 載入順序&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：兩個 stylesheet 都在 &lt;code>&amp;lt;head&amp;gt;&lt;/code> 載入。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>pagefind-ui.css 5-10KB、search.css（拆檔後）3-5KB&lt;/li>
&lt;li>兩者都阻塞首屏渲染（CSS render-blocking）&lt;/li>
&lt;li>加總 &amp;lt; 20KB、影響輕微&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：rare、僅在極慢網路下感受到。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：DevTools Network 看 CSS 下載時間。考慮：&lt;/p>
&lt;ul>
&lt;li>把 critical CSS inline（首屏需要的部分）、其他 lazy&lt;/li>
&lt;li>用 Hugo &lt;code>resources.Get | minify | fingerprint&lt;/code> 確保最小化&lt;/li>
&lt;/ul>
&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>全 critical path&lt;/td>
 &lt;td>慢&lt;/td>
 &lt;td>0（即可用）&lt;/td>
 &lt;td>小型站、所有資源都重要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lazy load 大型模組&lt;/td>
 &lt;td>快&lt;/td>
 &lt;td>中 — 使用者觸發才下載&lt;/td>
 &lt;td>搜尋、富互動模組&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Critical path + lazy mix&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>一般情境（pagefind 走這條）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service Worker preload&lt;/td>
 &lt;td>中 — 首次載完後永久快&lt;/td>
 &lt;td>0 — 從 cache 取&lt;/td>
 &lt;td>高頻使用者、PWA&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對搜尋頁的場景：&lt;strong>Lazy load 大型模組&lt;/strong>是 pagefind 預設行為、合理；考慮再進一步可以 preload entry chunk 在 idle 時。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資源載入時序的設計選擇是「首次渲染速度」與「首次互動延遲」的權衡 — 不是越早載越好。</strong> 把不影響首次渲染的資源延後（lazy load）、首屏更快；但延後的資源在使用者真正需要時可能還沒到、互動延遲。盤點時兩者一起看。</p>
<hr>
<h2 id="為什麼載入時序需要設計">為什麼載入時序需要設計</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>每個資源都有兩個時點：</p>
<table>
  <thead>
      <tr>
          <th>時點</th>
          <th>含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開始下載</td>
          <td>在 critical path（首屏）還是 lazy（首次互動才下載）</td>
      </tr>
      <tr>
          <td>可用</td>
          <td>下載完 + parse + 執行完</td>
      </tr>
  </tbody>
</table>
<p>把資源放 critical path = 阻塞首屏渲染；放 lazy = 首屏更快但首次互動可能等。</p>
<p>對搜尋頁：使用者打開 <code>/search/</code> 但可能不立刻搜尋 — pagefind index lazy load 是合理選擇。但若打開後立刻打字、index 還沒載完、第一次搜尋有明顯延遲。</p>
<h3 id="critical-path-vs-lazy-的標準">Critical path vs lazy 的標準</h3>
<table>
  <thead>
      <tr>
          <th>資源類型</th>
          <th>通常的選擇</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視覺主體 CSS（首屏看到的）</td>
          <td>Critical path</td>
      </tr>
      <tr>
          <td>互動 JS（事件處理）</td>
          <td>DOMContentLoaded 後即可</td>
      </tr>
      <tr>
          <td>大型功能模組（搜尋 index）</td>
          <td>Lazy、使用者觸發才載</td>
      </tr>
      <tr>
          <td>圖片 / 影片</td>
          <td>Lazy 視可見性</td>
      </tr>
  </tbody>
</table>
<p>選擇原則：<strong>「首屏渲染需要嗎？」是 → critical；「使用者一定會用嗎？」否 → lazy</strong>。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1pagefind-index-下載延遲">風險 1：Pagefind index 下載延遲</h3>
<p><strong>位置</strong>：PagefindUI 在 mount 時開始下載 entry chunk、之後才能搜尋。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>entry chunk（<code>pagefind-entry.json</code>）~ 10KB</li>
<li>下載 + parse 約 100-500ms（看網路）</li>
<li>使用者打開搜尋頁立刻打字時、第一個字可能還沒搜尋</li>
</ul>
<p><strong>症狀</strong>：使用者打開 /search/ 立刻打字、第一個字沒回應、過 200-500ms 才開始搜尋。</p>
<p><strong>第一個該查的</strong>：DevTools Network 看 entry chunk 下載時間。&gt; 500ms 考慮 preload 機制。</p>
<h3 id="風險-2個別-search-chunk-的-lazy-load">風險 2：個別 search chunk 的 lazy load</h3>
<p><strong>位置</strong>：使用者搜尋特定 term 時、pagefind 動態下載對應 chunk。</p>
<p><strong>判讀</strong>：每個搜尋 term 對應一個 chunk（依 term 前綴分）。第一次搜尋某個 prefix 要下載對應 chunk、之後同 prefix 搜尋走 cache。</p>
<p><strong>症狀</strong>：搜尋特定字時稍有延遲（200-500ms）、之後就快了。</p>
<p><strong>第一個該查的</strong>：Pagefind 內建 cache 機制、多數情境表現可接受。若極慢可考慮 service worker preload chunk。</p>
<h3 id="風險-3pagefind-ui-script-下載">風險 3：Pagefind UI script 下載</h3>
<p><strong>位置</strong>：<code>&lt;script src=&quot;/blog/pagefind/pagefind-ui.js&quot;&gt;</code>。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>~ 50KB minified、需在使用者打字前載完</li>
<li>有 <code>defer</code> 不阻塞 HTML parsing、但仍占 critical path 寬度</li>
</ul>
<p><strong>症狀</strong>：搜尋頁初次載入比一般頁慢。</p>
<p><strong>第一個該查的</strong>：確認 <code>&lt;script&gt;</code> 有 <code>defer</code> attribute、使用者開啟搜尋頁後背景下載、不阻塞 HTML 渲染。</p>
<h3 id="風險-4assetssearchcss-與-pagefind-uicss-載入順序">風險 4：assets/search.css 與 pagefind-ui.css 載入順序</h3>
<p><strong>位置</strong>：兩個 stylesheet 都在 <code>&lt;head&gt;</code> 載入。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>pagefind-ui.css 5-10KB、search.css（拆檔後）3-5KB</li>
<li>兩者都阻塞首屏渲染（CSS render-blocking）</li>
<li>加總 &lt; 20KB、影響輕微</li>
</ul>
<p><strong>症狀</strong>：rare、僅在極慢網路下感受到。</p>
<p><strong>第一個該查的</strong>：DevTools Network 看 CSS 下載時間。考慮：</p>
<ul>
<li>把 critical CSS inline（首屏需要的部分）、其他 lazy</li>
<li>用 Hugo <code>resources.Get | minify | fingerprint</code> 確保最小化</li>
</ul>
<hr>
<h2 id="內在屬性比較四種載入策略">內在屬性比較：四種載入策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>首屏速度</th>
          <th>首次互動延遲</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全 critical path</td>
          <td>慢</td>
          <td>0（即可用）</td>
          <td>小型站、所有資源都重要</td>
      </tr>
      <tr>
          <td>Lazy load 大型模組</td>
          <td>快</td>
          <td>中 — 使用者觸發才下載</td>
          <td>搜尋、富互動模組</td>
      </tr>
      <tr>
          <td>Critical path + lazy mix</td>
          <td>中</td>
          <td>低</td>
          <td>一般情境（pagefind 走這條）</td>
      </tr>
      <tr>
          <td>Service Worker preload</td>
          <td>中 — 首次載完後永久快</td>
          <td>0 — 從 cache 取</td>
          <td>高頻使用者、PWA</td>
      </tr>
  </tbody>
</table>
<p>對搜尋頁的場景：<strong>Lazy load 大型模組</strong>是 pagefind 預設行為、合理；考慮再進一步可以 preload entry chunk 在 idle 時。</p>
<hr>
<h2 id="preload-的取捨">Preload 的取捨</h2>
<p>預先載入下一步可能需要的資源 — 加快互動、但浪費頻寬（若使用者最終沒用）。</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="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;preload&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;/blog/pagefind/pagefind-entry.json&#34;</span> <span class="na">as</span><span class="o">=</span><span class="s">&#34;fetch&#34;</span> <span class="na">crossorigin</span><span class="p">&gt;</span></span></span></code></pre></div><p>放 head、瀏覽器在 critical path 完成後 idle 時開始下載。</p>
<p><strong>值得做的條件</strong>：</p>
<ul>
<li>使用者進入此頁的明確意圖會觸發該資源（搜尋頁進入 = 會搜尋）</li>
<li>資源不大（entry chunk &lt; 10KB OK）</li>
</ul>
<p><strong>不值得</strong>：</p>
<ul>
<li>使用者可能只看不用（首頁載 search index 通常不值得）</li>
<li>資源很大（不要 preload 整個 search index）</li>
</ul>
<hr>
<h2 id="設計取捨資源載入時序的策略">設計取捨：資源載入時序的策略</h2>
<p>四種做法、各自機會成本不同。預設按資源性質選 — 影響首屏 → A、使用者必用大型模組 → B、進入此頁必觸發 → C。</p>
<h3 id="acritical-path首屏阻塞">A：Critical path（首屏阻塞）</h3>
<ul>
<li><strong>機制</strong>：CSS <code>&lt;link&gt;</code> 在 head、JS 用 <code>defer</code> 在 head 或 body 末</li>
<li><strong>選 A 的理由</strong>：首屏渲染就需要、不能延後</li>
<li><strong>適合</strong>：視覺主體 CSS（首屏可見）、互動處理 JS（DOMContentLoaded 後即用）</li>
<li><strong>代價</strong>：阻塞首屏渲染、加總大小要控制（&lt; 50KB 為佳）</li>
</ul>
<h3 id="blazy-load使用者觸發才載">B：Lazy load（使用者觸發才載）</h3>
<ul>
<li><strong>機制</strong>：用動態 import / IntersectionObserver / 按 click 載入</li>
<li><strong>跟 A 的取捨</strong>：B 首屏快、A 首次互動快；B 在使用者必用時造成互動延遲</li>
<li><strong>B 比 A 好的情境</strong>：大型功能模組（搜尋 index、富文字編輯器）、使用者可能不用</li>
</ul>
<h3 id="cpreload打賭使用者會用">C：Preload（打賭使用者會用）</h3>
<ul>
<li><strong>機制</strong>：<code>&lt;link rel=&quot;preload&quot;&gt;</code> 在 idle 時下載、需要時從 cache 取</li>
<li><strong>跟 A/B 的取捨</strong>：C 不阻塞首屏（idle 下載）、需要時無延遲；但賭錯（使用者不用）就浪費頻寬</li>
<li><strong>C 比 A/B 好的情境</strong>：進入此頁的明確意圖會觸發該資源（搜尋頁進入 = 必搜尋）+ 資源不大（&lt; 10KB）</li>
</ul>
<h3 id="dservice-worker-預先-cache">D：Service Worker 預先 cache</h3>
<ul>
<li><strong>機制</strong>：第一次造訪時 cache 進 SW、之後從 cache 取</li>
<li><strong>跟 C 的取捨</strong>：D 第一次造訪後永久快、C 每次都要重新 preload；D 適合 PWA 等「重複造訪」場景</li>
<li><strong>D 比 C 好的情境</strong>：高頻使用者、PWA 應用、需要 offline 支援</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者打開頁面立刻互動有明顯延遲</td>
          <td>該互動依賴的資源是否 lazy、是否值得 preload</td>
      </tr>
      <tr>
          <td>首屏渲染慢、CSS / JS 阻塞</td>
          <td>DevTools Network 找 critical path 中可拆 lazy 的資源</td>
      </tr>
      <tr>
          <td>Lazy 資源永遠不被觸發</td>
          <td>該資源預設或許不必 lazy（不會 lazy 也不會貴）</td>
      </tr>
      <tr>
          <td>慢網路 / 行動裝置使用者抱怨</td>
          <td>用 DevTools Network throttling 模擬、量首屏與首次互動</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：載入時序是設計決定、不是預設。每個資源「critical / lazy / preload」三選一明確選、不要全部丟 critical path。</p>
]]></content:encoded></item><item><title>動態 DOM 移動時的 focus 管理</title><link>https://tarrragon.github.io/blog/report/focus-management-on-dom-move/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/focus-management-on-dom-move/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>JS 移動或隱藏 DOM 元素時、鍵盤 focus 的命運要主動處理 — 不處理會跑掉或停在不可見元素上、鍵盤使用者瞬間迷失方向。&lt;/strong> 多數動態 UI 的 focus 問題不是「某個元素該 focusable」、是「某個變動沒考慮 focus 該去哪」。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-focus-管理需要主動處理">為什麼 focus 管理需要主動處理&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>鍵盤使用者依 focus 知道「現在在哪」。focus 變動有三種來源：&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>使用者主動（Tab、Enter、方向鍵）&lt;/td>
 &lt;td>預期、無需處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Focus 元素被移除&lt;/td>
 &lt;td>focus 跳到 body — 使用者迷失&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Focus 元素被 reparent&lt;/td>
 &lt;td>看瀏覽器、可能 focus 仍在元素上、可能掉失&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第二、三類是 JS 變動 DOM 引起的副作用、開發者要主動處理。&lt;/p>
&lt;h3 id="三類-dom-變動對-focus-的影響">三類 DOM 變動對 focus 的影響&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變動類型&lt;/th>
 &lt;th>Focus 行為&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>整節點 reparent（appendChild）&lt;/td>
 &lt;td>視瀏覽器、Chrome 多半保留 focus、Safari 可能掉&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點 remove&lt;/td>
 &lt;td>focus 跳到 body&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點 display: none&lt;/td>
 &lt;td>focus 跳到 body&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點 visibility: hidden&lt;/td>
 &lt;td>focus 仍在但元素不可見、使用者迷失&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每類有對應的處理 — 主要是「事前 save、事後 restore」。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1filter-slot-跨-viewport-切換">風險 1：Filter slot 跨 viewport 切換&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：matchMedia callback 的 &lt;code>place()&lt;/code> 函式。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">place&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">2&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">slot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&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="k">else&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>判讀&lt;/strong>：使用者鍵盤 focus 在 filter 內某個 checkbox、視窗 resize 跨過 1400px、&lt;code>appendChild&lt;/code> 把 filter 整個搬到別處。理論上 focus 跟著節點走、實際視瀏覽器。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者按 tab 進到 filter checkbox、調視窗寬度跨 breakpoint、focus 突然在 body 或其他位置。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">place&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">activeBefore&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">activeElement&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">slot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&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="k">else&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 嘗試還原 focus
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">activeBefore&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">filter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">activeBefore&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">7&lt;/span>&lt;span class="cl"> &lt;span class="nx">activeBefore&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">focus&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>activeElement&lt;/code> 在 reparent 前後仍指向同一個 DOM 節點（如果 focus 在 filter 內）。明確 &lt;code>.focus()&lt;/code> 確保視覺一致。&lt;/p>
&lt;h3 id="風險-2scope-filter-隱藏當前-focus-元素">風險 2：Scope filter 隱藏當前 focus 元素&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：scope filter 的 &lt;code>apply()&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&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">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">toggle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;is-scope-filtered&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">show&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="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>判讀&lt;/strong>：若使用者 focus 在某個 result（例如標題連結）、切換 scope 後該 result 被隱藏（display: none）— focus 跳到 body。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者 tab 到 result、切 scope、focus 不見了。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">apply&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">activeBefore&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">activeElement&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="c1">// ... 套用 scope filter
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">activeBefore&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">getComputedStyle&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">activeBefore&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">display&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;none&amp;#39;&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"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 該元素被隱藏、focus 移到下一個可見的同類元素
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">nextResult&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">findNextVisibleResult&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">activeBefore&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">nextResult&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">nextResult&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">focus&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">else&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">focus&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 沒有下一個就回到 search input
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>明確處理「focus 元素被隱藏時去哪」、不留給瀏覽器預設行為。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>JS 移動或隱藏 DOM 元素時、鍵盤 focus 的命運要主動處理 — 不處理會跑掉或停在不可見元素上、鍵盤使用者瞬間迷失方向。</strong> 多數動態 UI 的 focus 問題不是「某個元素該 focusable」、是「某個變動沒考慮 focus 該去哪」。</p>
<hr>
<h2 id="為什麼-focus-管理需要主動處理">為什麼 focus 管理需要主動處理</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>鍵盤使用者依 focus 知道「現在在哪」。focus 變動有三種來源：</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者主動（Tab、Enter、方向鍵）</td>
          <td>預期、無需處理</td>
      </tr>
      <tr>
          <td>Focus 元素被移除</td>
          <td>focus 跳到 body — 使用者迷失</td>
      </tr>
      <tr>
          <td>Focus 元素被 reparent</td>
          <td>看瀏覽器、可能 focus 仍在元素上、可能掉失</td>
      </tr>
  </tbody>
</table>
<p>第二、三類是 JS 變動 DOM 引起的副作用、開發者要主動處理。</p>
<h3 id="三類-dom-變動對-focus-的影響">三類 DOM 變動對 focus 的影響</h3>
<table>
  <thead>
      <tr>
          <th>變動類型</th>
          <th>Focus 行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>整節點 reparent（appendChild）</td>
          <td>視瀏覽器、Chrome 多半保留 focus、Safari 可能掉</td>
      </tr>
      <tr>
          <td>節點 remove</td>
          <td>focus 跳到 body</td>
      </tr>
      <tr>
          <td>節點 display: none</td>
          <td>focus 跳到 body</td>
      </tr>
      <tr>
          <td>節點 visibility: hidden</td>
          <td>focus 仍在但元素不可見、使用者迷失</td>
      </tr>
  </tbody>
</table>
<p>每類有對應的處理 — 主要是「事前 save、事後 restore」。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1filter-slot-跨-viewport-切換">風險 1：Filter slot 跨 viewport 切換</h3>
<p><strong>位置</strong>：matchMedia callback 的 <code>place()</code> 函式。</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="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">slot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>判讀</strong>：使用者鍵盤 focus 在 filter 內某個 checkbox、視窗 resize 跨過 1400px、<code>appendChild</code> 把 filter 整個搬到別處。理論上 focus 跟著節點走、實際視瀏覽器。</p>
<p><strong>症狀</strong>：使用者按 tab 進到 filter checkbox、調視窗寬度跨 breakpoint、focus 突然在 body 或其他位置。</p>
<p><strong>第一個該查的</strong>：</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="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</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">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">slot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// 嘗試還原 focus
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>activeElement</code> 在 reparent 前後仍指向同一個 DOM 節點（如果 focus 在 filter 內）。明確 <code>.focus()</code> 確保視覺一致。</p>
<h3 id="風險-2scope-filter-隱藏當前-focus-元素">風險 2：Scope filter 隱藏當前 focus 元素</h3>
<p><strong>位置</strong>：scope filter 的 <code>apply()</code>。</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="nx">items</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s1">&#39;is-scope-filtered&#39;</span><span class="p">,</span> <span class="o">!</span><span class="nx">show</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p><strong>判讀</strong>：若使用者 focus 在某個 result（例如標題連結）、切換 scope 後該 result 被隱藏（display: none）— focus 跳到 body。</p>
<p><strong>症狀</strong>：使用者 tab 到 result、切 scope、focus 不見了。</p>
<p><strong>第一個該查的</strong>：</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="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c1">// ... 套用 scope filter
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">getComputedStyle</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">).</span><span class="nx">display</span> <span class="o">===</span> <span class="s1">&#39;none&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1">// 該元素被隱藏、focus 移到下一個可見的同類元素
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>    <span class="kd">var</span> <span class="nx">nextResult</span> <span class="o">=</span> <span class="nx">findNextVisibleResult</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">nextResult</span><span class="p">)</span> <span class="nx">nextResult</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">else</span> <span class="nx">input</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>   <span class="c1">// 沒有下一個就回到 search input
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>明確處理「focus 元素被隱藏時去哪」、不留給瀏覽器預設行為。</p>
<h3 id="風險-3pagefind-重繪結果時-focus-流失">風險 3：Pagefind 重繪結果時 focus 流失</h3>
<p><strong>位置</strong>：使用者改 query 時、pagefind 重新渲染結果列表。</p>
<p><strong>判讀</strong>：若使用者 tab 到第 1 個結果、修改 query、pagefind 替換整個結果列表 — 第 1 個結果被 remove、focus 跳到 body。</p>
<p><strong>症狀</strong>：使用者打字過程中、tab 順序時不時被打回起點。</p>
<p><strong>第一個該查的</strong>：這個情境較難解 — 框架管的 DOM 我們不能干預。可行的做法：</p>
<ul>
<li>使用者打字時通常在 input 上、focus 不在結果列表 — 影響面小</li>
<li>若真有需要、用 tabindex / aria-activedescendant 模擬 focus 但不實際 focus DOM</li>
</ul>
<h3 id="風險-4載入-pagefind-ui-時-focus-行為">風險 4：載入 pagefind UI 時 focus 行為</h3>
<p><strong>位置</strong>：頁面載入後 PagefindUI mount 約 200-500ms。</p>
<p><strong>判讀</strong>：使用者開啟搜尋頁、瀏覽器把 focus 放 body、使用者按 tab — 應該到搜尋輸入框。</p>
<p><strong>症狀</strong>：使用者開頁面立刻按 tab、focus 跳到網站其他部分（nav、其他 link）、不是搜尋框。</p>
<p><strong>第一個該查的</strong>：考慮頁面載入後自動 focus 搜尋輸入框（auto-focus）— 對搜尋頁是合理 UX、不是干擾。</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="nx">waitForElement</span><span class="p">(</span><span class="nx">searchRoot</span><span class="p">,</span> <span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">input</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">input</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><hr>
<h2 id="內在屬性比較四種-focus-處理策略">內在屬性比較：四種 focus 處理策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>維護成本</th>
          <th>涵蓋情境</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不處理（瀏覽器預設）</td>
          <td>低</td>
          <td>簡單情境</td>
          <td>focus 掉失常見</td>
      </tr>
      <tr>
          <td>Save / restore activeElement</td>
          <td>中</td>
          <td>DOM 移動、隱藏</td>
          <td>大多有效</td>
      </tr>
      <tr>
          <td>用 tabindex / aria-activedescendant 模擬 focus</td>
          <td>高</td>
          <td>框架管的 DOM</td>
          <td>較複雜、視框架行為</td>
      </tr>
      <tr>
          <td>Auto-focus 關鍵元素</td>
          <td>低</td>
          <td>頁面載入、modal 開啟</td>
          <td>使用者預期才適用</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>簡單變動用 save / restore；framework 管的 DOM 用模擬 focus；關鍵元素用 auto-focus</strong>。</p>
<hr>
<h2 id="盤點-focus-影響的具體步驟">盤點 focus 影響的具體步驟</h2>
<p>對每個 JS 變動 DOM 的位置、列三個問題：</p>
<ol>
<li><strong>這個變動會 reparent / remove / hide 哪些元素？</strong></li>
<li><strong>這些元素有可能是當前 focus 嗎？</strong> （form input、checkbox、link 都是常見 focusable）</li>
<li><strong>若是、focus 該去哪？</strong> （restore / next sibling / 預設位置）</li>
</ol>
<p>回答完三題、變動前後加 save / restore 邏輯。</p>
<hr>
<h2 id="設計取捨dom-變動時的-focus-處理策略">設計取捨：DOM 變動時的 focus 處理策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（save / restore activeElement）當預設、其他做法在特定情境合理。</p>
<h3 id="asave--restore-activeelement這個專案的預設">A：Save / restore activeElement（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：JS 變動 DOM 前 <code>var activeBefore = document.activeElement</code>、變動後 <code>activeBefore.focus()</code></li>
<li><strong>選 A 的理由</strong>：跨瀏覽器一致、簡單元件移動 / 顯隱都涵蓋</li>
<li><strong>適合</strong>：自家管的 DOM 變動（reparent、display: none、remove）</li>
<li><strong>代價</strong>：每個變動位置要顯式加 save / restore 邏輯（用 helper 包裝可一行）</li>
</ul>
<h3 id="b不處理依瀏覽器預設">B：不處理（依瀏覽器預設）</h3>
<ul>
<li><strong>機制</strong>：JS 變動 DOM、不主動處理 focus</li>
<li><strong>跟 A 的取捨</strong>：B 簡單、A 有額外邏輯；但 B 結果不一致（Chrome / Safari 不同）、多數預設是「focus 跳 body」、使用者迷失</li>
<li><strong>B 才合理的情境</strong>：純展示元素變動（沒有 focusable 子元素）— 不會發生 focus 掉失</li>
</ul>
<h3 id="c用-tabindex--aria-activedescendant-模擬-focus">C：用 tabindex / aria-activedescendant 模擬 focus</h3>
<ul>
<li><strong>機制</strong>：focus 物理上不動、用 attribute 標記「邏輯 focus」</li>
<li><strong>跟 A 的取捨</strong>：C 比 A 複雜、但能處理 framework 管的 DOM（無法 save / restore）</li>
<li><strong>C 比 A 好的情境</strong>：framework 持續重繪元素 identity、save / restore 失敗 — 用 attribute 表達 focus</li>
</ul>
<h3 id="dauto-focus-關鍵元素">D：Auto-focus 關鍵元素</h3>
<ul>
<li><strong>機制</strong>：頁面載入後 / modal 開啟後自動 focus 預期的第一個元素</li>
<li><strong>跟 A 的取捨</strong>：D 不依變動觸發、A 對應變動處理；D 適合「使用者預期」的初始 focus</li>
<li><strong>D 比 A 好的情境</strong>：搜尋頁載入 → focus search input、modal 開啟 → focus 第一個 input — 使用者預期的場景才用</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>鍵盤使用者 tab 中途 focus 突然跳走</td>
          <td>該時點是否有 JS 變動 DOM</td>
      </tr>
      <tr>
          <td>Resize 視窗後 focus 不見</td>
          <td>matchMedia callback 內加 save / restore</td>
      </tr>
      <tr>
          <td>切 filter / mode 後 focus 在 body</td>
          <td>apply 函式內處理被隱藏元素的 focus</td>
      </tr>
      <tr>
          <td>開頁面立刻按 tab 跳到不對位置</td>
          <td>評估是否該 auto-focus 主要互動元素</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：JS 變動 DOM = focus 副作用。每個變動位置都該回答「focus 該去哪」、不留給瀏覽器預設。</p>
<p>跟 <a href="../loading-empty-end-state-distinction/">#57 Loading / Empty / End 三狀態</a> 共骨：兩者都是「狀態變動需要回答對應的 UX 問題」 — #57 講「使用者看到的訊號」、本卡講「鍵盤使用者的 focus 位置」。動態 UI 設計 = 狀態變動 + 狀態變動的 UX + 狀態變動的 a11y 三個維度同時設計。</p>
]]></content:encoded></item><item><title>Screen reader 與動態內容變動的 live region 設計</title><link>https://tarrragon.github.io/blog/report/aria-live-for-dynamic-content/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/aria-live-for-dynamic-content/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>動態內容變動對螢幕報讀軟體使用者預設不可見 — 要主動透過 aria-live region 把變動「廣播」給輔助技術。&lt;/strong> 沒 live region 的 UI 在視覺使用者眼裡很流暢、在 screen reader 使用者眼裡是「靜悄悄變了什麼但我不知道」。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼動態內容需要主動廣播">為什麼動態內容需要主動廣播&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Screen reader 的工作模式：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>動作&lt;/th>
 &lt;th>screen reader 行為&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>頁面載入&lt;/td>
 &lt;td>朗讀整個 main 內容（或使用者導航位置）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者按 tab&lt;/td>
 &lt;td>朗讀新 focus 元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者按方向鍵&lt;/td>
 &lt;td>朗讀附近元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>DOM 變動但 focus 沒動&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>預設不朗讀&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第四種是動態 UI 的常見情境 — 使用者在 search input 上、結果列表變動 — screen reader 預設不知道。&lt;/p>
&lt;p>&lt;code>aria-live&lt;/code> 屬性告訴 screen reader「這個區域內容變動時、主動朗讀變動」。沒 aria-live、變動就沉默。&lt;/p>
&lt;h3 id="三類動態變動">三類動態變動&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變動類型&lt;/th>
 &lt;th>是否需要廣播&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>使用者主動觸發、focus 跟著走&lt;/td>
 &lt;td>否（focus 變動會朗讀新位置）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者主動觸發、focus 沒動&lt;/td>
 &lt;td>是 — 用 &lt;code>aria-live=&amp;quot;polite&amp;quot;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重要警示（錯誤訊息、警告）&lt;/td>
 &lt;td>是 — 用 &lt;code>aria-live=&amp;quot;assertive&amp;quot;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>polite&lt;/code> 等使用者當前朗讀完才宣告、&lt;code>assertive&lt;/code> 立刻打斷 — 多數場景用 polite。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1scope-filter-切換後沒提示">風險 1：Scope filter 切換後沒提示&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：scope filter 的 &lt;code>apply()&lt;/code> — 改變 result 顯示後沒有 aria 提示。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：使用者切換 scope（標題 / 內文 / 全部）、UI 上結果數量變了 — screen reader 完全不知道。使用者可能繼續以為「189 筆結果」、實際只剩 4 筆。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：screen reader 使用者切 scope 後、tab 到結果區、發現跟剛才不同、困惑。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：加 aria-live region 在 scope UI 旁邊、apply 後寫入訊息。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span> &lt;span class="na">role&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;radiogroup&amp;#34;&lt;/span> &lt;span class="na">aria-label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;搜尋範圍&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- radios... --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope-status&amp;#34;&lt;/span> &lt;span class="na">aria-live&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;polite&amp;#34;&lt;/span> &lt;span class="na">aria-atomic&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">apply&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">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... filter 邏輯
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">visible&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;is-scope-filtered&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">length&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="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope-status&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;篩選後 &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">visible&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; 筆結果&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>aria-atomic=&amp;quot;true&amp;quot;&lt;/code> 確保整個訊息每次都完整朗讀（而非只朗讀差異）。&lt;/p>
&lt;h3 id="風險-2搜尋結果載入完成沒提示">風險 2：搜尋結果載入完成沒提示&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：使用者打字、pagefind 載入結果 — UI 上 result 出現、但 screen reader 不知道載入完成。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：使用者打字結束、預期「結果出來了」— 但需要主動 tab 過去確認、不像視覺使用者一眼看到。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者打字後不知道結果是否準備好、不知道是否該 tab 過去。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：pagefind 自身可能已實作 aria-live；若未、加一個 region 在結果區上方。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-results-status&amp;#34;&lt;/span> &lt;span class="na">aria-live&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;polite&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">status&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">count&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; 筆結果符合搜尋&amp;#39;&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="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">resultsRoot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="風險-3filter-變動後沒提示">風險 3：Filter 變動後沒提示&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：使用者勾選 / 取消 filter checkbox、pagefind 自動更新結果。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：勾選某個 tag、結果列表變動 — screen reader 看不到變動、若 focus 還在 checkbox 也沒朗讀。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：螢幕報讀軟體使用者勾 filter、不知道有沒有效果。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：同上、aria-live region 反映「N 筆結果符合篩選」。&lt;/p>
&lt;h3 id="風險-4無結果訊息">風險 4：「無結果」訊息&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：搜尋字找不到任何結果。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：頁面顯示「找不到 X 相關內容」、screen reader 若 focus 還在 input 不會朗讀。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>動態內容變動對螢幕報讀軟體使用者預設不可見 — 要主動透過 aria-live region 把變動「廣播」給輔助技術。</strong> 沒 live region 的 UI 在視覺使用者眼裡很流暢、在 screen reader 使用者眼裡是「靜悄悄變了什麼但我不知道」。</p>
<hr>
<h2 id="為什麼動態內容需要主動廣播">為什麼動態內容需要主動廣播</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Screen reader 的工作模式：</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>screen reader 行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>頁面載入</td>
          <td>朗讀整個 main 內容（或使用者導航位置）</td>
      </tr>
      <tr>
          <td>使用者按 tab</td>
          <td>朗讀新 focus 元素</td>
      </tr>
      <tr>
          <td>使用者按方向鍵</td>
          <td>朗讀附近元素</td>
      </tr>
      <tr>
          <td><strong>DOM 變動但 focus 沒動</strong></td>
          <td><strong>預設不朗讀</strong></td>
      </tr>
  </tbody>
</table>
<p>第四種是動態 UI 的常見情境 — 使用者在 search input 上、結果列表變動 — screen reader 預設不知道。</p>
<p><code>aria-live</code> 屬性告訴 screen reader「這個區域內容變動時、主動朗讀變動」。沒 aria-live、變動就沉默。</p>
<h3 id="三類動態變動">三類動態變動</h3>
<table>
  <thead>
      <tr>
          <th>變動類型</th>
          <th>是否需要廣播</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者主動觸發、focus 跟著走</td>
          <td>否（focus 變動會朗讀新位置）</td>
      </tr>
      <tr>
          <td>使用者主動觸發、focus 沒動</td>
          <td>是 — 用 <code>aria-live=&quot;polite&quot;</code></td>
      </tr>
      <tr>
          <td>重要警示（錯誤訊息、警告）</td>
          <td>是 — 用 <code>aria-live=&quot;assertive&quot;</code></td>
      </tr>
  </tbody>
</table>
<p><code>polite</code> 等使用者當前朗讀完才宣告、<code>assertive</code> 立刻打斷 — 多數場景用 polite。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1scope-filter-切換後沒提示">風險 1：Scope filter 切換後沒提示</h3>
<p><strong>位置</strong>：scope filter 的 <code>apply()</code> — 改變 result 顯示後沒有 aria 提示。</p>
<p><strong>判讀</strong>：使用者切換 scope（標題 / 內文 / 全部）、UI 上結果數量變了 — screen reader 完全不知道。使用者可能繼續以為「189 筆結果」、實際只剩 4 筆。</p>
<p><strong>症狀</strong>：screen reader 使用者切 scope 後、tab 到結果區、發現跟剛才不同、困惑。</p>
<p><strong>第一個該查的</strong>：加 aria-live region 在 scope UI 旁邊、apply 後寫入訊息。</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="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="na">role</span><span class="o">=</span><span class="s">&#34;radiogroup&#34;</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;搜尋範圍&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c">&lt;!-- radios... --&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">class</span><span class="o">=</span><span class="s">&#34;search-scope-status&#34;</span> <span class="na">aria-live</span><span class="o">=</span><span class="s">&#34;polite&#34;</span> <span class="na">aria-atomic</span><span class="o">=</span><span class="s">&#34;true&#34;</span><span class="p">&gt;&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-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// ... filter 邏輯
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">visible</span> <span class="o">=</span> <span class="nx">items</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="o">!</span><span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="s1">&#39;is-scope-filtered&#39;</span><span class="p">)).</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope-status&#39;</span><span class="p">).</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;篩選後 &#39;</span> <span class="o">+</span> <span class="nx">visible</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="p">}</span></span></span></code></pre></div><p><code>aria-atomic=&quot;true&quot;</code> 確保整個訊息每次都完整朗讀（而非只朗讀差異）。</p>
<h3 id="風險-2搜尋結果載入完成沒提示">風險 2：搜尋結果載入完成沒提示</h3>
<p><strong>位置</strong>：使用者打字、pagefind 載入結果 — UI 上 result 出現、但 screen reader 不知道載入完成。</p>
<p><strong>判讀</strong>：使用者打字結束、預期「結果出來了」— 但需要主動 tab 過去確認、不像視覺使用者一眼看到。</p>
<p><strong>症狀</strong>：使用者打字後不知道結果是否準備好、不知道是否該 tab 過去。</p>
<p><strong>第一個該查的</strong>：pagefind 自身可能已實作 aria-live；若未、加一個 region 在結果區上方。</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="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-results-status&#34;</span> <span class="na">aria-live</span><span class="o">=</span><span class="s">&#34;polite&#34;</span><span class="p">&gt;&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-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">count</span> <span class="o">=</span> <span class="nx">items</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">status</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">count</span> <span class="o">+</span> <span class="s1">&#39; 筆結果符合搜尋&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">resultsRoot</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><h3 id="風險-3filter-變動後沒提示">風險 3：Filter 變動後沒提示</h3>
<p><strong>位置</strong>：使用者勾選 / 取消 filter checkbox、pagefind 自動更新結果。</p>
<p><strong>判讀</strong>：勾選某個 tag、結果列表變動 — screen reader 看不到變動、若 focus 還在 checkbox 也沒朗讀。</p>
<p><strong>症狀</strong>：螢幕報讀軟體使用者勾 filter、不知道有沒有效果。</p>
<p><strong>第一個該查的</strong>：同上、aria-live region 反映「N 筆結果符合篩選」。</p>
<h3 id="風險-4無結果訊息">風險 4：「無結果」訊息</h3>
<p><strong>位置</strong>：搜尋字找不到任何結果。</p>
<p><strong>判讀</strong>：頁面顯示「找不到 X 相關內容」、screen reader 若 focus 還在 input 不會朗讀。</p>
<p><strong>症狀</strong>：screen reader 使用者打字後沒任何回應、不知道是「無結果」還是「還在搜尋」。</p>
<p><strong>第一個該查的</strong>：把「無結果」訊息放 aria-live region 內、變動時自動朗讀。</p>
<hr>
<h2 id="live-region-的設計選擇">Live region 的設計選擇</h2>
<h3 id="polite-vs-assertive"><code>polite</code> vs <code>assertive</code></h3>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>行為</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>aria-live=&quot;polite&quot;</code></td>
          <td>等使用者當前朗讀完才宣告</td>
          <td>多數動態變動</td>
      </tr>
      <tr>
          <td><code>aria-live=&quot;assertive&quot;</code></td>
          <td>立刻打斷使用者朗讀</td>
          <td>錯誤、警告、緊急訊息</td>
      </tr>
  </tbody>
</table>
<p>優先 polite — assertive 容易打斷使用者、感覺很突兀。</p>
<h3 id="aria-atomic"><code>aria-atomic</code></h3>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>aria-atomic=&quot;false&quot;</code>（預設）</td>
          <td>只朗讀變動的部分</td>
      </tr>
      <tr>
          <td><code>aria-atomic=&quot;true&quot;</code></td>
          <td>整個 region 內容完整朗讀</td>
      </tr>
  </tbody>
</table>
<p>對「N 筆結果」這類固定格式訊息、用 <code>aria-atomic=&quot;true&quot;</code> 確保使用者聽到完整脈絡（不只朗讀數字變動）。</p>
<h3 id="aria-relevant"><code>aria-relevant</code></h3>
<p>預設只朗讀「新增 / 文字變動」、不朗讀「移除」。多數情境用預設即可。</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>0</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td><code>aria-live=&quot;polite&quot;</code></td>
          <td>大多數動態變動</td>
          <td>低 — 加 div 與 textContent 寫入</td>
          <td>預設</td>
      </tr>
      <tr>
          <td><code>aria-live=&quot;assertive&quot;</code></td>
          <td>緊急訊息</td>
          <td>低</td>
          <td>錯誤 / 警告</td>
      </tr>
      <tr>
          <td><code>role=&quot;status&quot;</code> / <code>role=&quot;alert&quot;</code></td>
          <td>semantic 角色明確</td>
          <td>低</td>
          <td>純 status / alert 元素</td>
      </tr>
  </tbody>
</table>
<p>優先選 <code>aria-live=&quot;polite&quot;</code> + <code>aria-atomic=&quot;true&quot;</code>、廣覆蓋且不打擾。</p>
<hr>
<h2 id="live-region-的常見錯誤">Live region 的常見錯誤</h2>
<h3 id="1-動態建立-region">1. 動態建立 region</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">var</span> <span class="nx">status</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">status</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;aria-live&#39;</span><span class="p">,</span> <span class="s1">&#39;polite&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">status</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;...&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</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">appendChild</span><span class="p">(</span><span class="nx">status</span><span class="p">);</span></span></span></code></pre></div><p>不會生效 — screen reader 在 region 出現「之後」變動才朗讀、region 從無到有的瞬間不算。</p>
<p>正確：region 在 HTML 預先存在、JS 只更新內容。</p>
<h3 id="2-全頁加一個共用-region">2. 全頁加一個共用 region</h3>
<p>可能導致訊息混淆 — 不同 source 的訊息共用同一個 region、難以追蹤。每個語意區域有自己的 region 較清楚。</p>
<h3 id="3-太頻繁的訊息">3. 太頻繁的訊息</h3>
<p>每次變動都廣播 — 使用者被 spam。Debounce + 重複內容跳過。</p>
<hr>
<h2 id="設計取捨動態內容廣播策略">設計取捨：動態內容廣播策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（aria-live polite + aria-atomic）當預設、其他做法在特定情境合理。</p>
<h3 id="aaria-livepolite--aria-atomictrue這個專案的預設">A：<code>aria-live=&quot;polite&quot;</code> + <code>aria-atomic=&quot;true&quot;</code>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：region 預先在 HTML、JS 寫入 textContent 觸發 polite 朗讀（等使用者當前朗讀完）</li>
<li><strong>選 A 的理由</strong>：覆蓋多數動態變動、不打擾使用者當前操作</li>
<li><strong>適合</strong>：搜尋結果數量變動、filter 切換、scope 改變等大多數 UI 變動</li>
<li><strong>代價</strong>：訊息要等使用者當前朗讀完才聽到（最多幾秒延遲）</li>
</ul>
<h3 id="baria-liveassertive">B：<code>aria-live=&quot;assertive&quot;</code></h3>
<ul>
<li><strong>機制</strong>：立刻打斷使用者當前朗讀、強制聽新訊息</li>
<li><strong>跟 A 的取捨</strong>：B 即時、A 禮貌；但 B 打斷感強、頻繁使用會讓使用者疲勞</li>
<li><strong>B 比 A 好的情境</strong>：真正緊急的訊息（錯誤 / 警告 / 安全提示）— 必須立刻知道</li>
</ul>
<h3 id="crolestatus--rolealert">C：<code>role=&quot;status&quot;</code> / <code>role=&quot;alert&quot;</code></h3>
<ul>
<li><strong>機制</strong>：用 semantic role 取代 aria-live、語意更明確</li>
<li><strong>跟 A 的取捨</strong>：C 跟 A 行為類似（status = polite、alert = assertive）、但 role 表達意圖更清楚</li>
<li><strong>C 比 A 好的情境</strong>：region 本身就是 status 或 alert 元素（語意對齊）</li>
</ul>
<h3 id="d不處理沉默">D：不處理（沉默）</h3>
<ul>
<li><strong>機制</strong>：DOM 變動不通知 screen reader</li>
<li><strong>成本特別高的原因</strong>：screen reader 使用者完全不知道有變動、UI 變得不可用</li>
<li><strong>D 才合理的情境</strong>：純視覺裝飾變動（背景動畫 / decorative）— 對 screen reader 使用者無意義</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Screen reader 使用者反映「不知道有沒有發生事」</td>
          <td>找出該變動位置、加 aria-live region</td>
      </tr>
      <tr>
          <td>動態 UI 沒任何 aria-live</td>
          <td>列出所有 focus 不跟著走的變動、各自評估是否需要</td>
      </tr>
      <tr>
          <td>Live region 朗讀但聽起來只有片段</td>
          <td>加 <code>aria-atomic=&quot;true&quot;</code></td>
      </tr>
      <tr>
          <td>訊息太頻繁打擾</td>
          <td>Debounce、跳過重複</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：UI 上「使用者沒主動觸發但有變動」的位置、screen reader 預設沉默 — 用 aria-live region 把沉默變成可聽見。</p>
<p>跟 <a href="../loading-empty-end-state-distinction/">#57 Loading / Empty / End 三狀態的區分</a> 同源：兩者都是「狀態變動需要告知使用者」、aria-live 告訴的是 screen reader、#57 講的是視覺區分。<strong>完整的狀態變動 UX = 視覺區分 + aria-live 廣播</strong>。</p>
]]></content:encoded></item><item><title>Native HTML element 優先於 ARIA role 的取捨</title><link>https://tarrragon.github.io/blog/report/native-html-over-aria-role/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/native-html-over-aria-role/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>有 native HTML element 提供的語意與行為時、永遠優先用 native — ARIA role 是「沒有 native 對應時的 fallback」、不是設計起點。&lt;/strong> Native element 自帶 keyboard、focus、screen reader 行為；ARIA 是給作者宣告 semantic 的工具、需要作者自己補完所有行為。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-native-永遠優先">為什麼 native 永遠優先&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>ARIA 規範自己有一條 first rule：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>First Rule of ARIA&lt;/strong>: If you can use a native HTML element with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, do so.&lt;/p>&lt;/blockquote>
&lt;p>理由是 native element 提供「semantic + behavior」雙包裝：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>包裝層&lt;/th>
 &lt;th>Native element&lt;/th>
 &lt;th>ARIA role&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Semantic（screen reader 知道是什麼）&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>Focus 行為&lt;/td>
 &lt;td>是（tab order、:focus）&lt;/td>
 &lt;td>否（要作者管 tabindex）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Form 整合&lt;/td>
 &lt;td>是（form submission、validation）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨瀏覽器一致&lt;/td>
 &lt;td>高（標準行為）&lt;/td>
 &lt;td>中（看 screen reader 解讀）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>ARIA role 只貼 semantic 標籤、不送行為 — 用 ARIA 等於承擔「補完所有行為」的責任。&lt;/p>
&lt;h3 id="何時-aria-是必要">何時 ARIA 是必要&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>ARIA 必要&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Native element 有對應功能&lt;/td>
 &lt;td>否 — 用 native&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要 semantic 但沒 native 對應&lt;/td>
 &lt;td>是 — 用 ARIA role&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>加強 native element 的描述（aria-label）&lt;/td>
 &lt;td>是 — ARIA 補強、不取代&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>動態狀態（aria-expanded、aria-checked）&lt;/td>
 &lt;td>是 — native 表達不了&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>ARIA 的設計用途是「補強 native」、不是「取代 native」。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1scope-ui-用-div-roleradiogroup-而非-fieldset">風險 1：Scope UI 用 &lt;code>div role=&amp;quot;radiogroup&amp;quot;&lt;/code> 而非 &lt;code>fieldset&lt;/code>&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span> &lt;span class="na">role&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;radiogroup&amp;#34;&lt;/span> &lt;span class="na">aria-label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;搜尋範圍&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">label&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;radio&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;all&amp;#34;&lt;/span> &lt;span class="na">checked&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>全部&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">label&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- ... --&amp;gt;&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">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>判讀&lt;/strong>：&lt;code>&amp;lt;div role=&amp;quot;radiogroup&amp;quot;&amp;gt;&lt;/code> 給 screen reader 看到「這是 radio group」、但作者要自己保證：&lt;/p>
&lt;ul>
&lt;li>鍵盤方向鍵在選項間切換&lt;/li>
&lt;li>Tab 行為符合 radiogroup 慣例（tab 進到 group、方向鍵在內切換、tab 出 group）&lt;/li>
&lt;li>aria-required / aria-invalid 等狀態同步&lt;/li>
&lt;/ul>
&lt;p>&lt;code>&amp;lt;fieldset&amp;gt;&amp;lt;legend&amp;gt;&lt;/code> 是 native element：&lt;/p>
&lt;ul>
&lt;li>自帶 group semantic&lt;/li>
&lt;li>legend 自動關聯為 group label&lt;/li>
&lt;li>內部 &lt;code>&amp;lt;input type=&amp;quot;radio&amp;quot; name=&amp;quot;X&amp;quot;&amp;gt;&lt;/code> 已是完整 radiogroup（HTML 內建）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：screen reader 可能不認得自訂 radiogroup、無法用方向鍵切換。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>有 native HTML element 提供的語意與行為時、永遠優先用 native — ARIA role 是「沒有 native 對應時的 fallback」、不是設計起點。</strong> Native element 自帶 keyboard、focus、screen reader 行為；ARIA 是給作者宣告 semantic 的工具、需要作者自己補完所有行為。</p>
<hr>
<h2 id="為什麼-native-永遠優先">為什麼 native 永遠優先</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>ARIA 規範自己有一條 first rule：</p>
<blockquote>
<p><strong>First Rule of ARIA</strong>: If you can use a native HTML element with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, do so.</p></blockquote>
<p>理由是 native element 提供「semantic + behavior」雙包裝：</p>
<table>
  <thead>
      <tr>
          <th>包裝層</th>
          <th>Native element</th>
          <th>ARIA role</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Semantic（screen reader 知道是什麼）</td>
          <td>是</td>
          <td>是</td>
      </tr>
      <tr>
          <td>鍵盤行為</td>
          <td>是（瀏覽器內建）</td>
          <td>否（要作者自己寫）</td>
      </tr>
      <tr>
          <td>Focus 行為</td>
          <td>是（tab order、:focus）</td>
          <td>否（要作者管 tabindex）</td>
      </tr>
      <tr>
          <td>Form 整合</td>
          <td>是（form submission、validation）</td>
          <td>否</td>
      </tr>
      <tr>
          <td>跨瀏覽器一致</td>
          <td>高（標準行為）</td>
          <td>中（看 screen reader 解讀）</td>
      </tr>
  </tbody>
</table>
<p>ARIA role 只貼 semantic 標籤、不送行為 — 用 ARIA 等於承擔「補完所有行為」的責任。</p>
<h3 id="何時-aria-是必要">何時 ARIA 是必要</h3>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>ARIA 必要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Native element 有對應功能</td>
          <td>否 — 用 native</td>
      </tr>
      <tr>
          <td>需要 semantic 但沒 native 對應</td>
          <td>是 — 用 ARIA role</td>
      </tr>
      <tr>
          <td>加強 native element 的描述（aria-label）</td>
          <td>是 — ARIA 補強、不取代</td>
      </tr>
      <tr>
          <td>動態狀態（aria-expanded、aria-checked）</td>
          <td>是 — native 表達不了</td>
      </tr>
  </tbody>
</table>
<p>ARIA 的設計用途是「補強 native」、不是「取代 native」。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1scope-ui-用-div-roleradiogroup-而非-fieldset">風險 1：Scope UI 用 <code>div role=&quot;radiogroup&quot;</code> 而非 <code>fieldset</code></h3>
<p><strong>位置</strong>：</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="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="na">role</span><span class="o">=</span><span class="s">&#34;radiogroup&#34;</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;搜尋範圍&#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">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;all&#34;</span> <span class="na">checked</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>全部<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <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">div</span><span class="p">&gt;</span></span></span></code></pre></div><p><strong>判讀</strong>：<code>&lt;div role=&quot;radiogroup&quot;&gt;</code> 給 screen reader 看到「這是 radio group」、但作者要自己保證：</p>
<ul>
<li>鍵盤方向鍵在選項間切換</li>
<li>Tab 行為符合 radiogroup 慣例（tab 進到 group、方向鍵在內切換、tab 出 group）</li>
<li>aria-required / aria-invalid 等狀態同步</li>
</ul>
<p><code>&lt;fieldset&gt;&lt;legend&gt;</code> 是 native element：</p>
<ul>
<li>自帶 group semantic</li>
<li>legend 自動關聯為 group label</li>
<li>內部 <code>&lt;input type=&quot;radio&quot; name=&quot;X&quot;&gt;</code> 已是完整 radiogroup（HTML 內建）</li>
</ul>
<p><strong>症狀</strong>：screen reader 可能不認得自訂 radiogroup、無法用方向鍵切換。</p>
<p><strong>第一個該查的</strong>：用 NVDA / VoiceOver 進入 radiogroup、按方向鍵看是否能切換。失敗則改用 fieldset。</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="p">&lt;</span><span class="nt">fieldset</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#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">legend</span><span class="p">&gt;</span>搜尋範圍<span class="p">&lt;/</span><span class="nt">legend</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">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;all&#34;</span> <span class="na">checked</span><span class="p">&gt;</span> 全部<span class="p">&lt;/</span><span class="nt">label</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">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;title&#34;</span><span class="p">&gt;</span> 標題<span class="p">&lt;/</span><span class="nt">label</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">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;content&#34;</span><span class="p">&gt;</span> 內文<span class="p">&lt;/</span><span class="nt">label</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">fieldset</span><span class="p">&gt;</span></span></span></code></pre></div><p><code>name=&quot;search-scope&quot;</code> 同名讓三個 radio 自動成為 group、HTML 自帶方向鍵切換。</p>
<h3 id="風險-2用-div-onclick-取代-button">風險 2：用 <code>&lt;div onclick&gt;</code> 取代 <code>&lt;button&gt;</code></h3>
<p><strong>位置</strong>：自訂按鈕 UI（搜尋頁未必有、但常見 anti-pattern）。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>&lt;button&gt;</code> 自帶 enter / space 觸發、tab focus、disabled 狀態</li>
<li><code>&lt;div onclick&gt;</code> 只有 click 事件、鍵盤無法觸發、tab 不會 focus</li>
</ul>
<p><strong>症狀</strong>：鍵盤使用者無法操作該 UI。</p>
<p><strong>第一個該查的</strong>：找 <code>&lt;div onclick&gt;</code> / <code>&lt;span onclick&gt;</code> 的 pattern、改為 <code>&lt;button&gt;</code>。</p>
<h3 id="風險-3pagefind-自身的-aria-實作">風險 3：Pagefind 自身的 ARIA 實作</h3>
<p><strong>位置</strong>：Pagefind 的 <code>&lt;details&gt;&lt;summary&gt;</code> filter blocks。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>&lt;details&gt;</code> / <code>&lt;summary&gt;</code> 是 native element、自帶 expand / collapse、enter 切換</li>
<li>Pagefind 包了 <code>.pagefind-ui__filter-name</code> class 但底層仍是 native — 行為跟著</li>
<li>這是好的設計、不需要動</li>
</ul>
<p><strong>症狀</strong>：rare、native element 多半 OK。</p>
<p><strong>第一個該查的</strong>：確認 Pagefind 沒用 div+role 重新實作這些 — 從 source 看大致符合 native first principle。</p>
<h3 id="風險-4search-input-用-input-typesearch-還是-input-typetext">風險 4：Search input 用 <code>&lt;input type=&quot;search&quot;&gt;</code> 還是 <code>&lt;input type=&quot;text&quot;&gt;</code></h3>
<p><strong>位置</strong>：Pagefind 自身的 input。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>&lt;input type=&quot;search&quot;&gt;</code> 在 mobile 顯示「搜尋」鍵盤、自帶清除按鈕</li>
<li><code>&lt;input type=&quot;text&quot;&gt;</code> 純文字輸入</li>
</ul>
<p><strong>症狀</strong>：mobile 鍵盤不適配搜尋場景、額外清除 UI 自己做。</p>
<p><strong>第一個該查的</strong>：確認 Pagefind 用 <code>type=&quot;search&quot;</code>。從 pagefind-ui 渲染結果可看到 <code>type=&quot;text&quot;</code>、有自訂的清除按鈕 — 可考慮是否值得改。</p>
<hr>
<h2 id="內在屬性比較四種實作-radio-group-的方式">內在屬性比較：四種實作 radio group 的方式</h2>
<table>
  <thead>
      <tr>
          <th>實作</th>
          <th>鍵盤切換</th>
          <th>screen reader 認</th>
          <th>維護成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>&lt;fieldset&gt;&lt;legend&gt;</code> + <code>&lt;input type=&quot;radio&quot;&gt;</code> × N</td>
          <td>是 — HTML 內建</td>
          <td>是 — fieldset semantic</td>
          <td>低</td>
      </tr>
      <tr>
          <td><code>&lt;div role=&quot;radiogroup&quot;&gt;</code> + <code>&lt;input type=&quot;radio&quot;&gt;</code> × N</td>
          <td>是 — input radio 自帶</td>
          <td>部分 — div role 跟 input semantic 重複</td>
          <td>中</td>
      </tr>
      <tr>
          <td><code>&lt;div role=&quot;radiogroup&quot;&gt;</code> + <code>&lt;div role=&quot;radio&quot;&gt;</code> × N</td>
          <td>否 — 要自己寫</td>
          <td>是 — 但需作者完整實作 ARIA pattern</td>
          <td>高</td>
      </tr>
      <tr>
          <td>純自訂無 ARIA</td>
          <td>否</td>
          <td>否</td>
          <td>不適用</td>
      </tr>
  </tbody>
</table>
<p>優先順序：<strong>fieldset &gt; div role + native input &gt; div role + div role</strong>。</p>
<hr>
<h2 id="aria-使用的判斷流程">ARIA 使用的判斷流程</h2>
<p>每個 UI 元素開始實作前、走這個流程：</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. 有沒有 native element 對應？
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   是 → 用 native（fieldset、button、input、details / summary）
</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">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">2. 有沒有 ARIA pattern 對應？
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   是 → 用 div + role + 完整 ARIA 屬性 + 自己寫鍵盤行為
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   否 → 進 3
</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">3. 用 div + 自己想 semantic
</span></span><span class="line"><span class="ln">10</span><span class="cl">   注意：可能 screen reader 不認得、需要充分測試</span></span></code></pre></div><p>多數情境停在 1 — native HTML 涵蓋常見 UI 模式。需要走到 2、3 的場景比想像中少。</p>
<hr>
<h2 id="設計取捨實作-ui-元素的策略">設計取捨：實作 UI 元素的策略</h2>
<p>四種做法、各自機會成本不同。這個專案永遠優先選 A（native HTML element）— 不夠用才退到 B / C / D。</p>
<h3 id="a純-native-html-element永遠的首選">A：純 native HTML element（永遠的首選）</h3>
<ul>
<li><strong>機制</strong>：用 <code>&lt;button&gt;</code>、<code>&lt;fieldset&gt;&lt;legend&gt;</code>、<code>&lt;details&gt;&lt;summary&gt;</code>、<code>&lt;input type=&quot;search&quot;&gt;</code> 等 native 元素</li>
<li><strong>選 A 的理由</strong>：semantic + 鍵盤 + focus + form 整合「四件套」自帶、跨瀏覽器一致、跨 screen reader 一致</li>
<li><strong>適合</strong>：所有 native 涵蓋的 UI 模式（按鈕、表單、disclosure、radio group）</li>
<li><strong>代價</strong>：受 native 視覺預設限制、客製樣式可能要對抗 UA 預設</li>
</ul>
<h3 id="bnative--aria-補強aria-label--aria-describedby--aria-expanded">B：Native + ARIA 補強（aria-label / aria-describedby / aria-expanded）</h3>
<ul>
<li><strong>機制</strong>：native element 加 ARIA 屬性補強 semantic 或表達動態狀態</li>
<li><strong>跟 A 的取捨</strong>：B 在 A 的基礎上加細節、不取代</li>
<li><strong>B 比 A 好的情境</strong>：native 已涵蓋主要功能、需要補額外資訊（label、描述）或動態狀態（expanded / pressed / checked）</li>
</ul>
<h3 id="cdiv-rolex--完整-aria-pattern--自寫鍵盤行為">C：<code>&lt;div role=&quot;X&quot;&gt;</code> + 完整 ARIA pattern + 自寫鍵盤行為</h3>
<ul>
<li><strong>機制</strong>：用 div 包成 semantic 元素、加 role + 完整 ARIA + JS 補鍵盤</li>
<li><strong>跟 A 的取捨</strong>：C 給更高客製彈性、A 拿成熟方案；C 維護成本高（要自己保證所有行為）</li>
<li><strong>C 比 A 好的情境</strong>：native 沒對應的 UI 模式（complex tree view、custom slider）— 必須自己定義 semantic</li>
</ul>
<h3 id="d純-div--自訂-semantic無-aria">D：純 div + 自訂 semantic（無 ARIA）</h3>
<ul>
<li><strong>機制</strong>：用 div 自己想 semantic、不加 role</li>
<li><strong>成本特別高的原因</strong>：screen reader 不認得、鍵盤無法操作、違反 a11y 標準</li>
<li><strong>D 是反模式</strong>：違反 a11y 標準（屬合規 / 法規層）— 純視覺裝飾元素（無互動）才能例外</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用 <code>&lt;div role=&quot;X&quot;&gt;</code> 取代有 native 的 element</td>
          <td>評估改用 native、減少 ARIA 維護</td>
      </tr>
      <tr>
          <td>自訂 UI 鍵盤無法操作</td>
          <td>改用 native button / input、自帶鍵盤行為</td>
      </tr>
      <tr>
          <td>自訂 form 元素跟 form submission 不整合</td>
          <td>改用 native input、自動加入 form data</td>
      </tr>
      <tr>
          <td>Screen reader 不一致地解讀 ARIA</td>
          <td>改用 native、多數 screen reader 對 native 一致</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：ARIA 的 first rule 是「能用 native 就不用 ARIA」。Native element 是 50 年累積的瀏覽器 + 輔助技術知識結晶、不要繞道。</p>
]]></content:encoded></item><item><title>視覺輔助：對比度、放大、字型 zoom 的 layout 適配</title><link>https://tarrragon.github.io/blog/report/visual-aids-contrast-zoom-responsive/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/visual-aids-contrast-zoom-responsive/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>視覺輔助使用者跟一般使用者「看到的不是同一個 UI」 — 對比度、放大倍率、字型尺寸調整都會把版面變形。&lt;/strong> 設計時先盤點「在這些變形下、UI 還能用嗎」、不需要等到使用者反映。WCAG 提供量化標準、可以在開發階段驗證。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>視覺呈現面的 a11y&lt;/strong>（對比 / 放大 / 字型 zoom）。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>鍵盤使用者的 a11y&lt;/strong>（focus indicator / tab 順序）由 &lt;a href="../keyboard-accessibility/">#52 鍵盤可達性&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>行動 / motor 使用者的 a11y&lt;/strong>（hit target / 點擊精準度）由 &lt;a href="../motor-accessibility-hit-target/">#53 Motor 可達性&lt;/a> 處理&lt;/li>
&lt;/ul>&lt;/blockquote>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>色弱（colour blindness）&lt;/td>
 &lt;td>不依賴顏色區分資訊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低對比敏感&lt;/td>
 &lt;td>文字 vs 背景對比足夠&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低視力（low vision）&lt;/td>
 &lt;td>字大、可放大、layout 不破&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>老花、暫時視覺受限&lt;/td>
 &lt;td>字大、清楚的視覺層次&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每類觸發不同的 CSS 行為。一個 UI 在標準視窗看起來 OK、放大 200% 後可能：&lt;/p>
&lt;ul>
&lt;li>字超出容器&lt;/li>
&lt;li>Absolute 定位元件跑到視窗外&lt;/li>
&lt;li>對比度被覆蓋（dark mode / 高對比模式）&lt;/li>
&lt;/ul>
&lt;p>WCAG（Web Content Accessibility Guidelines）提供量化標準（對比度 AA ≥ 4.5:1、放大 200% 不橫向 scroll）— 可在開發階段測量。&lt;/p>
&lt;h3 id="視覺呈現的三維度">視覺呈現的三維度&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>變形方式&lt;/th>
 &lt;th>開發階段檢查方法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>色彩&lt;/td>
 &lt;td>dark mode / 高對比模式 / 色弱模擬&lt;/td>
 &lt;td>DevTools Contrast Ratio + Emulate Vision Deficiencies&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>整體 zoom&lt;/td>
 &lt;td>瀏覽器 zoom 200% / OS 放大鏡&lt;/td>
 &lt;td>Cmd + 5 次、macOS Zoom 4x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>字型 zoom&lt;/td>
 &lt;td>OS Display Scale（只放大字型不放大 box）&lt;/td>
 &lt;td>OS 設定 Larger Text&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三維度獨立、要分開檢查 — 一維度過 ≠ 全部過。&lt;/p>
&lt;hr>
&lt;h2 id="風險點-1搜尋結果-highlight-對比度">風險點 1：搜尋結果 highlight 對比度&lt;/h2>
&lt;p>&lt;strong>位置&lt;/strong>：Pagefind 高亮命中關鍵字（黃底）。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>預設 &lt;code>--pagefind-ui-tag&lt;/code> = &lt;code>#eeeeee&lt;/code>（淺灰）— 文字 &lt;code>#393939&lt;/code>（深灰）、對比 ~9:1、合格&lt;/li>
&lt;li>但搜尋頁 dark mode 下、theme 可能讓文字變淺色 — 對淺底要驗證&lt;/li>
&lt;li>色弱使用者看不出哪個字是 highlight（若僅靠顏色區分）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>WCAG 標準&lt;/strong>：&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>AA 一般文字&lt;/td>
 &lt;td>≥ 4.5:1&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AA 大字（≥ 18pt 或 14pt bold）&lt;/td>
 &lt;td>≥ 3:1&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AAA 一般文字&lt;/td>
 &lt;td>≥ 7:1&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：用 Chrome DevTools 的 Contrast Ratio 工具量 highlight 區域的「背景 vs 文字」對比。不足則覆寫 &lt;code>--pagefind-ui-tag&lt;/code> 變數。&lt;/p>
&lt;p>&lt;strong>雙重保險&lt;/strong>：除了顏色、加 underline 或 bold 區分 highlight — 色弱使用者不靠顏色也能辨識。&lt;/p>
&lt;hr>
&lt;h2 id="風險點-2absolute-定位元件在放大模式下跑到視窗外">風險點 2：Absolute 定位元件在放大模式下跑到視窗外&lt;/h2>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;code>.search-filter-slot { position: absolute; right: calc(100% + 2rem); }&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>視覺輔助使用者跟一般使用者「看到的不是同一個 UI」 — 對比度、放大倍率、字型尺寸調整都會把版面變形。</strong> 設計時先盤點「在這些變形下、UI 還能用嗎」、不需要等到使用者反映。WCAG 提供量化標準、可以在開發階段驗證。</p>
<blockquote>
<p>本篇焦點：<strong>視覺呈現面的 a11y</strong>（對比 / 放大 / 字型 zoom）。</p>
<ul>
<li><strong>鍵盤使用者的 a11y</strong>（focus indicator / tab 順序）由 <a href="../keyboard-accessibility/">#52 鍵盤可達性</a> 處理</li>
<li><strong>行動 / motor 使用者的 a11y</strong>（hit target / 點擊精準度）由 <a href="../motor-accessibility-hit-target/">#53 Motor 可達性</a> 處理</li>
</ul></blockquote>
<hr>
<h2 id="為什麼視覺輔助需要獨立盤點">為什麼視覺輔助需要獨立盤點</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>視覺輔助使用者的需求多元：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>需求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>色弱（colour blindness）</td>
          <td>不依賴顏色區分資訊</td>
      </tr>
      <tr>
          <td>低對比敏感</td>
          <td>文字 vs 背景對比足夠</td>
      </tr>
      <tr>
          <td>低視力（low vision）</td>
          <td>字大、可放大、layout 不破</td>
      </tr>
      <tr>
          <td>老花、暫時視覺受限</td>
          <td>字大、清楚的視覺層次</td>
      </tr>
  </tbody>
</table>
<p>每類觸發不同的 CSS 行為。一個 UI 在標準視窗看起來 OK、放大 200% 後可能：</p>
<ul>
<li>字超出容器</li>
<li>Absolute 定位元件跑到視窗外</li>
<li>對比度被覆蓋（dark mode / 高對比模式）</li>
</ul>
<p>WCAG（Web Content Accessibility Guidelines）提供量化標準（對比度 AA ≥ 4.5:1、放大 200% 不橫向 scroll）— 可在開發階段測量。</p>
<h3 id="視覺呈現的三維度">視覺呈現的三維度</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>變形方式</th>
          <th>開發階段檢查方法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>色彩</td>
          <td>dark mode / 高對比模式 / 色弱模擬</td>
          <td>DevTools Contrast Ratio + Emulate Vision Deficiencies</td>
      </tr>
      <tr>
          <td>整體 zoom</td>
          <td>瀏覽器 zoom 200% / OS 放大鏡</td>
          <td>Cmd + 5 次、macOS Zoom 4x</td>
      </tr>
      <tr>
          <td>字型 zoom</td>
          <td>OS Display Scale（只放大字型不放大 box）</td>
          <td>OS 設定 Larger Text</td>
      </tr>
  </tbody>
</table>
<p>三維度獨立、要分開檢查 — 一維度過 ≠ 全部過。</p>
<hr>
<h2 id="風險點-1搜尋結果-highlight-對比度">風險點 1：搜尋結果 highlight 對比度</h2>
<p><strong>位置</strong>：Pagefind 高亮命中關鍵字（黃底）。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>預設 <code>--pagefind-ui-tag</code> = <code>#eeeeee</code>（淺灰）— 文字 <code>#393939</code>（深灰）、對比 ~9:1、合格</li>
<li>但搜尋頁 dark mode 下、theme 可能讓文字變淺色 — 對淺底要驗證</li>
<li>色弱使用者看不出哪個字是 highlight（若僅靠顏色區分）</li>
</ul>
<p><strong>WCAG 標準</strong>：</p>
<table>
  <thead>
      <tr>
          <th>等級</th>
          <th>對比度要求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AA 一般文字</td>
          <td>≥ 4.5:1</td>
      </tr>
      <tr>
          <td>AA 大字（≥ 18pt 或 14pt bold）</td>
          <td>≥ 3:1</td>
      </tr>
      <tr>
          <td>AAA 一般文字</td>
          <td>≥ 7:1</td>
      </tr>
  </tbody>
</table>
<p><strong>第一個該查的</strong>：用 Chrome DevTools 的 Contrast Ratio 工具量 highlight 區域的「背景 vs 文字」對比。不足則覆寫 <code>--pagefind-ui-tag</code> 變數。</p>
<p><strong>雙重保險</strong>：除了顏色、加 underline 或 bold 區分 highlight — 色弱使用者不靠顏色也能辨識。</p>
<hr>
<h2 id="風險點-2absolute-定位元件在放大模式下跑到視窗外">風險點 2：Absolute 定位元件在放大模式下跑到視窗外</h2>
<p><strong>位置</strong>：<code>.search-filter-slot { position: absolute; right: calc(100% + 2rem); }</code>。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>Absolute 定位相對 main 計算</li>
<li>使用者用 OS 螢幕放大鏡（macOS Zoom）放大 4x 看 main 中央</li>
<li>main 仍在視窗範圍、但 absolute filter 在 main 左外側 — 放大 4x 後可能完全跑到視窗左邊看不見</li>
</ul>
<p><strong>症狀</strong>：低視力使用者用放大鏡時、不知道 filter 存在、無法操作。</p>
<p><strong>第一個該查的</strong>：用 macOS 的 Zoom 功能（System Settings &gt; Accessibility &gt; Zoom）放大 4x、看 filter 是否仍在可達範圍。</p>
<p><strong>修正方向</strong>：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>放大模式 fallback 到 mobile layout</td>
          <td><code>@media</code> 偵測 prefers-reduced-motion / 高 zoom level</td>
      </tr>
      <tr>
          <td>Filter 移到頁面內 flow（不用 absolute）</td>
          <td>跟主要內容一起 reflow、不會跑外</td>
      </tr>
      <tr>
          <td>加 floating button「展開 filter」</td>
          <td>任何 zoom level 都可達</td>
      </tr>
  </tbody>
</table>
<p>詳細展開由 <a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a> + <a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a> 補充。</p>
<hr>
<h2 id="風險點-3字型放大-200-後-layout-破壞">風險點 3：字型放大 200% 後 layout 破壞</h2>
<p><strong>位置</strong>：所有寫死 px 高度的元素（H1、search input、filter slot padding）。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>使用者用瀏覽器 zoom（Cmd +）通常等比放大 — 字 + box 一起放大、layout 不破</li>
<li>但 OS Display Scale（macOS Display &gt; Larger Text）只放大字型不放大 box — 字撐爆寫死的 64px 高度</li>
</ul>
<p>當 H1 字撐到 80px、寫死 height: 64px 的 box — 字被裁切。</p>
<p><strong>症狀</strong>：低視力使用者開啟「文字放大」設定、UI 字被裁。</p>
<p><strong>第一個該查的</strong>：開瀏覽器 zoom 200%、看 layout 是否變橫向 scroll（破壞）或仍 reflow（OK）。</p>
<p><strong>WCAG 標準</strong>：1.4.4 Resize text — zoom 至 200% 時不需要橫向 scroll。</p>
<p><strong>修正方向</strong>：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用 <code>min-height</code> 取代 <code>height</code></td>
          <td>box 可隨字撐高、不裁切</td>
      </tr>
      <tr>
          <td>用 <code>em</code> / <code>rem</code> 取代 <code>px</code></td>
          <td>跟字型一起 scale</td>
      </tr>
      <tr>
          <td>用 ResizeObserver 量字型實際高度寫回變數</td>
          <td>跟 <a href="../runtime-measurement-unification/">#27 runtime 量測模式統一</a> 同框架</td>
      </tr>
  </tbody>
</table>
<p>預設用 <code>min-height</code> + 相對單位、特殊精準對齊才用 ResizeObserver。</p>
<hr>
<h2 id="設計取捨layout-適應字型放大">設計取捨：layout 適應字型放大</h2>
<p>當「對齊精度」與「字型放大相容性」衝突、四種做法：</p>
<h3 id="a用-min-height--相對單位這個專案的預設">A：用 <code>min-height</code> + 相對單位（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>min-height: 4rem</code>、box 隨字撐高、用 <code>em</code> / <code>rem</code> 跟著 scale</li>
<li><strong>選 A 的理由</strong>：字型放大時 layout 自然 reflow、不裁切</li>
<li><strong>適合</strong>：絕大多數 UI 元素、不需要極精準對齊</li>
<li><strong>代價</strong>：對齊精度受字型 metrics 影響、難以做 pixel-perfect 對齊</li>
</ul>
<h3 id="b寫死-height--resizeobserver-量測補償">B：寫死 <code>height</code> + ResizeObserver 量測補償</h3>
<ul>
<li><strong>機制</strong>：<code>height: 64px</code>、用 ResizeObserver 量實際渲染高度寫回 CSS 變數、其他依賴此值的元素跟著調</li>
<li><strong>跟 A 的取捨</strong>：B 達到 pixel-perfect 對齊、A 信任 reflow；B 多一層 JS 量測、A 純 CSS</li>
<li><strong>B 比 A 好的情境</strong>：對齊精度是 UX 核心（搜尋頁的視覺對齊）、字型可預期</li>
</ul>
<h3 id="c寫死-height--不處理字型放大">C：寫死 <code>height</code> + 不處理字型放大</h3>
<ul>
<li><strong>機制</strong>：<code>height: 64px</code>、不管字型放大</li>
<li><strong>成本特別高的原因</strong>：字型放大時 UI 被裁切、低視力使用者無法用</li>
<li><strong>C 才合理的情境</strong>：UI 不會被字型放大影響（純圖示、無文字）</li>
</ul>
<h3 id="d用-clampmin-ideal-max-限制字型大小">D：用 <code>clamp(min, ideal, max)</code> 限制字型大小</h3>
<ul>
<li><strong>機制</strong>：字型 <code>font-size: clamp(0.875rem, 1rem, 1.125rem)</code>、限制使用者放大範圍</li>
<li><strong>跟 A/B/C 的取捨</strong>：D 主動限制字型放大範圍、違反 WCAG 1.4.4</li>
<li><strong>D 是反模式</strong>：違反 WCAG 1.4.4 — 強制限制字型放大是反 a11y、低視力使用者完全無法調整</li>
</ul>
<hr>
<h2 id="開發階段檢查清單">開發階段檢查清單</h2>
<p>每個視覺輔助項目對應一個檢查動作：</p>
<table>
  <thead>
      <tr>
          <th>檢查</th>
          <th>動作</th>
          <th>WCAG 等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對比度</td>
          <td>DevTools Inspect Element &gt; Contrast Ratio 看每個文字區域</td>
          <td>AA 必要</td>
      </tr>
      <tr>
          <td>色彩可辨</td>
          <td>DevTools Rendering &gt; Emulate Vision Deficiencies</td>
          <td>AA 建議</td>
      </tr>
      <tr>
          <td>Zoom 200%</td>
          <td>瀏覽器 Cmd + 5 次、看是否仍可用、無橫向 scroll</td>
          <td>AA 必要</td>
      </tr>
      <tr>
          <td>OS 字型放大</td>
          <td>macOS Display &gt; Text Size &gt; 大、看 layout</td>
          <td>AA 建議</td>
      </tr>
      <tr>
          <td>螢幕放大鏡</td>
          <td>macOS Zoom 4x、看絕對定位元件是否在可達範圍</td>
          <td>AA 建議</td>
      </tr>
  </tbody>
</table>
<p>每個 ~30 秒、開發完成前跑一輪、抓常見問題。</p>
<hr>
<h2 id="設計取捨色彩區分策略">設計取捨：色彩區分策略</h2>
<p>當資訊需要區分（hit / miss、selected / unselected）、四種做法：</p>
<h3 id="a顏色--形狀--位置雙重區分這個專案的預設">A：顏色 + 形狀 / 位置雙重區分（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：highlight 用黃底 + bold；selected radio 用色彩 + ✓ 圖示</li>
<li><strong>選 A 的理由</strong>：色弱使用者不靠顏色仍能辨識</li>
<li><strong>適合</strong>：絕大多數需要區分資訊的場景</li>
<li><strong>代價</strong>：UI 多一層視覺裝飾</li>
</ul>
<h3 id="b純顏色區分">B：純顏色區分</h3>
<ul>
<li><strong>機制</strong>：紅色 = 錯、綠色 = 對</li>
<li><strong>跟 A 的取捨</strong>：B 視覺乾淨、A 對色弱友善；B 違反 WCAG 1.4.1 Use of Color</li>
<li><strong>B 是反模式</strong>：違反 WCAG 1.4.1 Use of Color（合規層） — 色弱使用者完全無法區分對 / 錯</li>
</ul>
<h3 id="c純形狀--位置區分無顏色">C：純形狀 / 位置區分（無顏色）</h3>
<ul>
<li><strong>機制</strong>：用 ✓ / ✗ / 位置區分、不靠顏色</li>
<li><strong>跟 A 的取捨</strong>：C 對色彩無關、A 對視力正常使用者更直覺</li>
<li><strong>C 比 A 好的情境</strong>：列印 / 黑白渲染環境</li>
</ul>
<h3 id="d使用者可自訂顏色">D：使用者可自訂顏色</h3>
<ul>
<li><strong>機制</strong>：透過 CSS variable 讓使用者覆寫色彩</li>
<li><strong>跟 A 的取捨</strong>：D 提供無限彈性、實作成本高</li>
<li><strong>D 才合理的情境</strong>：core a11y 工具（如 reading mode）</li>
</ul>
<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>字型放大下 layout 適配是「不依賴特定渲染條件」的應用</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSoT</a></td>
          <td>CSS 變數提供主題切換、變數住址唯一才能正確覆寫色彩</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>色弱使用者反映找不到資訊</td>
          <td>DevTools Contrast Ratio + Emulate Vision Deficiencies</td>
      </tr>
      <tr>
          <td>低視力使用者反映 UI 跑到視窗外</td>
          <td>用螢幕放大鏡放 4x 確認 absolute 元件位置</td>
      </tr>
      <tr>
          <td>字型放大後 UI 破</td>
          <td>用瀏覽器 zoom 200% 與 OS text size 雙測</td>
      </tr>
      <tr>
          <td>Dark mode 下文字看不清</td>
          <td>該主題的對比度未驗證、補測</td>
      </tr>
      <tr>
          <td>「色弱使用者反正不多」當不做的理由</td>
          <td>視覺輔助使用者通常不會反映、只默默離開 — 量化檢查不靠使用者通報</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：視覺輔助使用者用的是「同一份程式、不同的 viewport / colour / scale」。WCAG 提供量化標準、開發階段可測 — 等使用者反映晚了。</p>
]]></content:encoded></item><item><title>Mode 與 Facet 是不同語意層級、UI 區域分開擺放</title><link>https://tarrragon.github.io/blog/report/mode-vs-facet-semantics/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mode-vs-facet-semantics/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Mode（模式）跟 Facet（多面向篩選）是兩個語意層級、UI 區域必須分開。&lt;/strong> Mode 決定「如何搜」、Facet 決定「篩選什麼結果」 — 兩者作用點不同、混在同一 UI 區會讓使用者誤以為是同層的選項、產生「為什麼勾這個結果這麼少」的困惑。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼語意層級要對應視覺分區">為什麼語意層級要對應視覺分區&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>搜尋 UI 包含的控制看起來都是「縮小結果」、語意層級實際不同：&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>Mode&lt;/td>
 &lt;td>搜尋演算法本身&lt;/td>
 &lt;td>「如何搜」（範圍、方法）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Facet&lt;/td>
 &lt;td>已搜結果集&lt;/td>
 &lt;td>「篩什麼結果」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Mode 在「搜尋」之前生效、Facet 在「搜尋」之後生效 — 兩者在 query pipeline 的位置不同。&lt;/p>
&lt;h3 id="混在一起的失敗模式">混在一起的失敗模式&lt;/h3>
&lt;p>把 mode 跟 facet 放在同一 UI 區域、使用者預設兩者作用相同：&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>勾「標題」是過濾標題欄位（facet）&lt;/td>
 &lt;td>Mode 改變搜尋範圍（標題不含的字會直接 0 結果）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>取消勾「標題」會擴大結果&lt;/td>
 &lt;td>取消等於切回「全部」mode、搜尋邏輯整個換&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>使用者看不出「為什麼結果差異這麼大」 — 因為以為換了一個 facet、實際換了搜尋演算法。&lt;/p>
&lt;h3 id="ui-慣例位置">UI 慣例位置&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>控制類型&lt;/th>
 &lt;th>UI 慣例位置&lt;/th>
 &lt;th>視覺暗示&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Mode&lt;/td>
 &lt;td>緊貼輸入框（旁邊或下方）&lt;/td>
 &lt;td>「跟搜尋本身綁在一起」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Facet&lt;/td>
 &lt;td>結果區附近 / sidebar&lt;/td>
 &lt;td>「在結果上做的事」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>慣例位置反映語意層級 — mode 屬於「query 構造」、facet 屬於「結果處理」。&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>Mode（影響 regex 比對範圍）&lt;/td>
 &lt;td>一開始想塞進 filter 區&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter：Type / Tag&lt;/td>
 &lt;td>Facet（在已搜結果上篩選）&lt;/td>
 &lt;td>Pagefind 預設 sidebar / drawer&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>把 scope 放進 filter 區會讓使用者把它當第三種 facet — 但「全部 / 標題 / 內文」不是篩選現有結果、是換搜尋範圍：&lt;/p>
&lt;ul>
&lt;li>勾「標題」：搜尋只跑標題欄位、內文有的字直接 0 結果&lt;/li>
&lt;li>取消「標題」（切回「全部」）：搜尋範圍擴大、結果集完全不同&lt;/li>
&lt;/ul>
&lt;p>如果使用者以為這是 facet、預期「取消會稍微多幾個結果」 — 實際結果集大幅變動會困惑。&lt;/p>
&lt;h3 id="執行分區擺放">執行：分區擺放&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>控制&lt;/th>
 &lt;th>最終位置&lt;/th>
 &lt;th>視覺距離&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Scope（mode）&lt;/td>
 &lt;td>搜尋輸入框正下方&lt;/td>
 &lt;td>緊鄰 — 跟 input 視覺綁在一起&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter（facet）&lt;/td>
 &lt;td>左側 sidebar（≥ 1400px）或 pagefind drawer（&amp;lt; 1400px）&lt;/td>
 &lt;td>跟結果區一起&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者視覺上明顯分開、使用者可區分「這是改搜尋方式」vs「這是篩結果」。&lt;/p>
&lt;hr>
&lt;h2 id="內在屬性比較三種-modefacet-擺放">內在屬性比較：三種 mode/facet 擺放&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>全部塞同一區（filter）&lt;/td>
 &lt;td>高 — mode 被誤當 facet&lt;/td>
 &lt;td>低 — 不分區&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mode 在 input 旁、Facet 在 sidebar&lt;/td>
 &lt;td>低 — 視覺暗示明確&lt;/td>
 &lt;td>中 — 兩個 slot&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mode 在 input 旁、Facet 在 sidebar、加說明文字&lt;/td>
 &lt;td>最低 — 雙重提示&lt;/td>
 &lt;td>中 — 多文字&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優先選「分區 + 視覺暗示」 — 不需要文字說明、靠位置即可辨識。&lt;/p>
&lt;hr>
&lt;h2 id="進階多個-mode-並存的處理">進階：多個 mode 並存的處理&lt;/h2>
&lt;p>當有多個 mode（搜尋範圍 + 排序方式 + 語言）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Mode 類型&lt;/th>
 &lt;th>慣例位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>搜尋範圍&lt;/td>
 &lt;td>input 下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>排序方式（相關度 / 日期）&lt;/td>
 &lt;td>結果區頂端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語言切換&lt;/td>
 &lt;td>全站 header（最高層級）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 mode 跟它影響的範圍視覺鄰近 — 影響搜尋範圍的靠 input、影響結果排序的靠結果。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Mode（模式）跟 Facet（多面向篩選）是兩個語意層級、UI 區域必須分開。</strong> Mode 決定「如何搜」、Facet 決定「篩選什麼結果」 — 兩者作用點不同、混在同一 UI 區會讓使用者誤以為是同層的選項、產生「為什麼勾這個結果這麼少」的困惑。</p>
<hr>
<h2 id="為什麼語意層級要對應視覺分區">為什麼語意層級要對應視覺分區</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>搜尋 UI 包含的控制看起來都是「縮小結果」、語意層級實際不同：</p>
<table>
  <thead>
      <tr>
          <th>控制類型</th>
          <th>作用對象</th>
          <th>改變的東西</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mode</td>
          <td>搜尋演算法本身</td>
          <td>「如何搜」（範圍、方法）</td>
      </tr>
      <tr>
          <td>Facet</td>
          <td>已搜結果集</td>
          <td>「篩什麼結果」</td>
      </tr>
  </tbody>
</table>
<p>Mode 在「搜尋」之前生效、Facet 在「搜尋」之後生效 — 兩者在 query pipeline 的位置不同。</p>
<h3 id="混在一起的失敗模式">混在一起的失敗模式</h3>
<p>把 mode 跟 facet 放在同一 UI 區域、使用者預設兩者作用相同：</p>
<table>
  <thead>
      <tr>
          <th>使用者誤判</th>
          <th>實際發生</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>勾「標題」是過濾標題欄位（facet）</td>
          <td>Mode 改變搜尋範圍（標題不含的字會直接 0 結果）</td>
      </tr>
      <tr>
          <td>取消勾「標題」會擴大結果</td>
          <td>取消等於切回「全部」mode、搜尋邏輯整個換</td>
      </tr>
  </tbody>
</table>
<p>使用者看不出「為什麼結果差異這麼大」 — 因為以為換了一個 facet、實際換了搜尋演算法。</p>
<h3 id="ui-慣例位置">UI 慣例位置</h3>
<table>
  <thead>
      <tr>
          <th>控制類型</th>
          <th>UI 慣例位置</th>
          <th>視覺暗示</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mode</td>
          <td>緊貼輸入框（旁邊或下方）</td>
          <td>「跟搜尋本身綁在一起」</td>
      </tr>
      <tr>
          <td>Facet</td>
          <td>結果區附近 / sidebar</td>
          <td>「在結果上做的事」</td>
      </tr>
  </tbody>
</table>
<p>慣例位置反映語意層級 — mode 屬於「query 構造」、facet 屬於「結果處理」。</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>Mode（影響 regex 比對範圍）</td>
          <td>一開始想塞進 filter 區</td>
      </tr>
      <tr>
          <td>Filter：Type / Tag</td>
          <td>Facet（在已搜結果上篩選）</td>
          <td>Pagefind 預設 sidebar / drawer</td>
      </tr>
  </tbody>
</table>
<h3 id="判讀">判讀</h3>
<p>把 scope 放進 filter 區會讓使用者把它當第三種 facet — 但「全部 / 標題 / 內文」不是篩選現有結果、是換搜尋範圍：</p>
<ul>
<li>勾「標題」：搜尋只跑標題欄位、內文有的字直接 0 結果</li>
<li>取消「標題」（切回「全部」）：搜尋範圍擴大、結果集完全不同</li>
</ul>
<p>如果使用者以為這是 facet、預期「取消會稍微多幾個結果」 — 實際結果集大幅變動會困惑。</p>
<h3 id="執行分區擺放">執行：分區擺放</h3>
<table>
  <thead>
      <tr>
          <th>控制</th>
          <th>最終位置</th>
          <th>視覺距離</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope（mode）</td>
          <td>搜尋輸入框正下方</td>
          <td>緊鄰 — 跟 input 視覺綁在一起</td>
      </tr>
      <tr>
          <td>Filter（facet）</td>
          <td>左側 sidebar（≥ 1400px）或 pagefind drawer（&lt; 1400px）</td>
          <td>跟結果區一起</td>
      </tr>
  </tbody>
</table>
<p>兩者視覺上明顯分開、使用者可區分「這是改搜尋方式」vs「這是篩結果」。</p>
<hr>
<h2 id="內在屬性比較三種-modefacet-擺放">內在屬性比較：三種 mode/facet 擺放</h2>
<table>
  <thead>
      <tr>
          <th>擺放策略</th>
          <th>使用者誤判風險</th>
          <th>實作成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全部塞同一區（filter）</td>
          <td>高 — mode 被誤當 facet</td>
          <td>低 — 不分區</td>
      </tr>
      <tr>
          <td>Mode 在 input 旁、Facet 在 sidebar</td>
          <td>低 — 視覺暗示明確</td>
          <td>中 — 兩個 slot</td>
      </tr>
      <tr>
          <td>Mode 在 input 旁、Facet 在 sidebar、加說明文字</td>
          <td>最低 — 雙重提示</td>
          <td>中 — 多文字</td>
      </tr>
  </tbody>
</table>
<p>優先選「分區 + 視覺暗示」 — 不需要文字說明、靠位置即可辨識。</p>
<hr>
<h2 id="進階多個-mode-並存的處理">進階：多個 mode 並存的處理</h2>
<p>當有多個 mode（搜尋範圍 + 排序方式 + 語言）：</p>
<table>
  <thead>
      <tr>
          <th>Mode 類型</th>
          <th>慣例位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>搜尋範圍</td>
          <td>input 下方</td>
      </tr>
      <tr>
          <td>排序方式（相關度 / 日期）</td>
          <td>結果區頂端</td>
      </tr>
      <tr>
          <td>語言切換</td>
          <td>全站 header（最高層級）</td>
      </tr>
  </tbody>
</table>
<p>每個 mode 跟它影響的範圍視覺鄰近 — 影響搜尋範圍的靠 input、影響結果排序的靠結果。</p>
<p>Facet 也可以分多區（type/tag 在 sidebar、價格區間在結果上方）— 但 mode 與 facet 之間永遠保持區域分離。</p>
<hr>
<h2 id="設計取捨搜尋控制的擺放策略">設計取捨：搜尋控制的擺放策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（Mode 靠 input + Facet 靠結果）當預設、其他做法在特定情境合理。</p>
<h3 id="amode-緊貼-input--facet-靠近結果這個專案的預設">A：Mode 緊貼 input + Facet 靠近結果（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：mode 控制（搜尋範圍 / 排序）放 input 旁；facet 控制（type / tag）放結果區或 sidebar</li>
<li><strong>選 A 的理由</strong>：位置就是契約 — 靠 input 的影響搜尋本身、靠結果的篩選結果；使用者靠位置辨識語意層級、不需文字說明</li>
<li><strong>適合</strong>：搜尋 UI、有 mode + facet 兩種控制</li>
<li><strong>代價</strong>：UI 拆兩個區、空間使用多一些</li>
</ul>
<h3 id="b所有控制集中-sidebar">B：所有控制集中 sidebar</h3>
<ul>
<li><strong>機制</strong>：mode + facet 都放 sidebar 一起</li>
<li><strong>跟 A 的取捨</strong>：B 集中管理視覺乾淨、A 分區語意清晰；但 B 使用者區分不了哪個影響 query、哪個影響結果</li>
<li><strong>B 是反模式</strong>：mode/facet 混淆是 UX 痛點 — 使用者區分不了哪個影響 query、哪個影響結果</li>
</ul>
<h3 id="c所有控制集中-input-旁">C：所有控制集中 input 旁</h3>
<ul>
<li><strong>機制</strong>：mode + facet 都靠 input、結果區無控制</li>
<li><strong>跟 A 的取捨</strong>：C 操作集中、A 按語意分；但 C facet 跟結果脫鉤、視覺暗示錯</li>
<li><strong>C 比 A 好的情境</strong>：facet 數量極少（&lt; 3 個）、放 input 旁不擁擠</li>
</ul>
<h3 id="d加文字說明取代位置暗示">D：加文字說明取代位置暗示</h3>
<ul>
<li><strong>機制</strong>：UI 加「此項目影響搜尋演算法」「此項目篩選結果」說明</li>
<li><strong>跟 A 的取捨</strong>：D 文字精確、A 靠位置直覺；但 D 增加閱讀負擔、使用者通常跳過說明</li>
<li><strong>D 比 A 好的情境</strong>：複雜搜尋介面（多種 mode、多層 facet）— 純位置暗示不夠</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>對應問題</th>
          <th>修正動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者問「為什麼這個結果出現了」</td>
          <td>mode/facet 混淆</td>
          <td>確認控制類型、分區擺放</td>
      </tr>
      <tr>
          <td>取消某個篩選、結果集大幅變動</td>
          <td>該控制是 mode 不是 facet</td>
          <td>移到 input 旁、視覺區隔</td>
      </tr>
      <tr>
          <td>控制集中在 sidebar、有些影響搜尋有些篩結果</td>
          <td>全部塞 filter 區</td>
          <td>拆出 mode 區、靠 input</td>
      </tr>
      <tr>
          <td>新增搜尋功能不知道該放哪</td>
          <td>沒有分區慣例</td>
          <td>先判斷 mode/facet、再決定位置</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：搜尋 UI 的控制不是同層的「篩選器」 — 語意層級不同、視覺分區也應不同。位置就是契約：靠 input 的影響搜尋本身、靠結果的篩選結果。</p>
<p>跟 <a href="../filter-instruction-clarification/">#58 篩選類指令的澄清時機</a> 的關係：mode 跟 facet 是兩種不同的「篩選類指令」。Mode（如「title-only / content / both」）通常重塑 query → 對應 #58 三問的「定義域 (c) 重新搜尋」；Facet（如「type=post tag=js」）通常加 filter 條件 → 對應「定義域 (b) 在所有結果裡找」。語意分區是視覺面、定義域選擇是行為面 — 兩者一起設計才完整。</p>
]]></content:encoded></item><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>Pattern：Document 全文件 query</title><link>https://tarrragon.github.io/blog/report/pattern-document-query/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-document-query/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從整個頁面找元素、不指定 ancestor scope。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>簡潔。一行就能取到目標、不需要先建立元件根變數。在「我只想快速確認某個元素在不在 / 取它的某個屬性」這類情境下、寫一個 import shell 變數 + null check 是過度工程。&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>Devtools console 一行查詢&lt;/td>
 &lt;td>沒有「未來會壞」的問題、用完就丟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>原型 / spike 階段程式碼&lt;/td>
 &lt;td>預期會被丟棄重寫、不需要長期維護考量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>確定全頁唯一的單例（&lt;code>document.body&lt;/code>、&lt;code>&amp;lt;html&amp;gt;&lt;/code>）&lt;/td>
 &lt;td>從定義上不會多個、也不會被誤命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build-time script、不會在 runtime 跑&lt;/td>
 &lt;td>沒有「同頁多元件」的可能性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>核心特徵：&lt;strong>這段程式不會在多元件 / 動態 DOM 環境長期存活&lt;/strong>。&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>Production 客製、預期長期存活&lt;/td>
 &lt;td>未來頁面結構變動、誤命中或漏命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同頁可能有多個同類元件&lt;/td>
 &lt;td>只取第一個、其他被忽略且不報錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件可能在 SPA 路由中動態增減&lt;/td>
 &lt;td>query 時機跟元件 mount 時機不對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入第三方函式庫&lt;/td>
 &lt;td>使用者頁面的其他 class 可能跟你的 selector 撞&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>安靜失敗是最危險的特徵&lt;/strong> — 不報錯、操作了錯元素、bug 表現遠離 root cause。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法、document query 是其中之一：&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>本卡片：document query&lt;/td>
 &lt;td>簡潔但不防護未來變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>多一行 setup、換到「shell 內隔離」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>多實例支援、適合可能擴展的客製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>事件委派情境、動態元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇順序：production 客製預設用「元件根變數」、原型 / 探索 / 一次性才用 document query。&lt;/p>
&lt;hr>
&lt;h2 id="邊界什麼時候-document-query-在-production-也合理">邊界：什麼時候 document query 在 production 也合理&lt;/h2>
&lt;p>幾個常見的 production 例外：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 1：操作的目標就是「全頁面唯一單例」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;page-search&amp;#39;&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="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-theme&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;dark&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 2：跨元件邊界的元素（不在任何元件內）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">slot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">// (slot 是 main 的子節點、不在 search-shell 內、不能從 shell 找)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 3：頁面層級的 meta 元素
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;meta[name=&amp;#34;description&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例外都共享一個特徵：&lt;strong>目標元素本質上就在「頁面層級」、不是任何元件的內部&lt;/strong>。&lt;/p>
&lt;p>不是例外的場景、即使「當前頁面只有一個」、也用元件根變數 — 預防未來擴展。&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>「現在只有一個、之後再想」&lt;/td>
 &lt;td>是 — 換元件根變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同檔案多處 &lt;code>document.querySelector('.x')&lt;/code>&lt;/td>
 &lt;td>是 — 至少改成存變數重用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫第三方 library 用 document query&lt;/td>
 &lt;td>是 — 改用根參數 pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作 &lt;code>document.body&lt;/code> / &lt;code>&amp;lt;html&amp;gt;&lt;/code>&lt;/td>
 &lt;td>否 — 這就是合理場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式跑一次後丟棄（migration script）&lt;/td>
 &lt;td>否 — 簡潔優先&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：document query 不是反模式、是有適用範圍的工具。判斷「這段程式預期活多久」 — 短命用 document、長命用元件根。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span></span></span></code></pre></div><p>從整個頁面找元素、不指定 ancestor scope。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>簡潔。一行就能取到目標、不需要先建立元件根變數。在「我只想快速確認某個元素在不在 / 取它的某個屬性」這類情境下、寫一個 import shell 變數 + null check 是過度工程。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Devtools console 一行查詢</td>
          <td>沒有「未來會壞」的問題、用完就丟</td>
      </tr>
      <tr>
          <td>原型 / spike 階段程式碼</td>
          <td>預期會被丟棄重寫、不需要長期維護考量</td>
      </tr>
      <tr>
          <td>確定全頁唯一的單例（<code>document.body</code>、<code>&lt;html&gt;</code>）</td>
          <td>從定義上不會多個、也不會被誤命中</td>
      </tr>
      <tr>
          <td>Build-time script、不會在 runtime 跑</td>
          <td>沒有「同頁多元件」的可能性</td>
      </tr>
  </tbody>
</table>
<p>核心特徵：<strong>這段程式不會在多元件 / 動態 DOM 環境長期存活</strong>。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production 客製、預期長期存活</td>
          <td>未來頁面結構變動、誤命中或漏命中</td>
      </tr>
      <tr>
          <td>同頁可能有多個同類元件</td>
          <td>只取第一個、其他被忽略且不報錯</td>
      </tr>
      <tr>
          <td>元件可能在 SPA 路由中動態增減</td>
          <td>query 時機跟元件 mount 時機不對齊</td>
      </tr>
      <tr>
          <td>寫入第三方函式庫</td>
          <td>使用者頁面的其他 class 可能跟你的 selector 撞</td>
      </tr>
  </tbody>
</table>
<p><strong>安靜失敗是最危險的特徵</strong> — 不報錯、操作了錯元素、bug 表現遠離 root cause。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法、document query 是其中之一：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本卡片：document query</td>
          <td>簡潔但不防護未來變動</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>多一行 setup、換到「shell 內隔離」</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>多實例支援、適合可能擴展的客製</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>事件委派情境、動態元件</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：production 客製預設用「元件根變數」、原型 / 探索 / 一次性才用 document query。</p>
<hr>
<h2 id="邊界什麼時候-document-query-在-production-也合理">邊界：什麼時候 document query 在 production 也合理</h2>
<p>幾個常見的 production 例外：</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">// 例外 1：操作的目標就是「全頁面唯一單例」
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;page-search&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-theme&#39;</span><span class="p">,</span> <span class="s1">&#39;dark&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 例外 2：跨元件邊界的元素（不在任何元件內）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">slot</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-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// (slot 是 main 的子節點、不在 search-shell 內、不能從 shell 找)
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 例外 3：頁面層級的 meta 元素
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;meta[name=&#34;description&#34;]&#39;</span><span class="p">);</span></span></span></code></pre></div><p>例外都共享一個特徵：<strong>目標元素本質上就在「頁面層級」、不是任何元件的內部</strong>。</p>
<p>不是例外的場景、即使「當前頁面只有一個」、也用元件根變數 — 預防未來擴展。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換做法嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「現在只有一個、之後再想」</td>
          <td>是 — 換元件根變數</td>
      </tr>
      <tr>
          <td>同檔案多處 <code>document.querySelector('.x')</code></td>
          <td>是 — 至少改成存變數重用</td>
      </tr>
      <tr>
          <td>寫第三方 library 用 document query</td>
          <td>是 — 改用根參數 pattern</td>
      </tr>
      <tr>
          <td>操作 <code>document.body</code> / <code>&lt;html&gt;</code></td>
          <td>否 — 這就是合理場景</td>
      </tr>
      <tr>
          <td>程式跑一次後丟棄（migration script）</td>
          <td>否 — 簡潔優先</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：document query 不是反模式、是有適用範圍的工具。判斷「這段程式預期活多久」 — 短命用 document、長命用元件根。</p>
]]></content:encoded></item><item><title>Pattern：元件根變數 query</title><link>https://tarrragon.github.io/blog/report/pattern-component-root/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-component-root/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">// ... 之後所有 query 都從 shell 開始
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把元件根 query 一次存變數、所有後續 query 都從這個變數開始。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把 selector 的作用範圍從「全頁面」收斂到「元件內部」。即使未來頁面其他地方出現同名元素、跟我無關。成本只多一行 query + 一個 null check、防護收益大。&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>Production 客製、預期長期存活&lt;/td>
 &lt;td>未來頁面結構可能變動、需要隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>當前只有一個元件實例、未來可能加&lt;/td>
 &lt;td>提早預防、改造成本最低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件根 mount 後不會被移除&lt;/td>
 &lt;td>變數生命週期跟元件一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式跑在頁面 mount 後（DOMContentLoaded 後）&lt;/td>
 &lt;td>shell 可被找到&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：寫的時候只有一個元件、但希望程式碼能容忍未來頁面結構變動。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>同頁同時有多個元件實例&lt;/td>
 &lt;td>變數只存第一個 shell、其他被忽略&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件動態增減（SPA 路由切換）&lt;/td>
 &lt;td>變數指向 stale DOM&lt;/td>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次性 / 探索期程式&lt;/td>
 &lt;td>過度工程&lt;/td>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="null-check-的時機">Null check 的時機&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會 null pointer。提早 return 比後續一連串 &lt;code>if (drawer)&lt;/code> 乾淨。&lt;/p>
&lt;p>&lt;strong>等同於宣告&lt;/strong>：「這段程式只在有 shell 的頁面執行」。&lt;/p>
&lt;h3 id="變數的宣告位置">變數的宣告位置&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>函式內 local 變數&lt;/td>
 &lt;td>預設、scope 最小&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Module scope（IIFE 內）&lt;/td>
 &lt;td>多函式共用同一 shell&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Class instance property&lt;/td>
 &lt;td>元件本身用 class 包裝時&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>避免全域變數 — &lt;code>window.shell&lt;/code> 容易跟其他 script 撞。&lt;/p>
&lt;h3 id="等待-shell-mount-的處理">等待 shell mount 的處理&lt;/h3>
&lt;p>如果 script 跑得太早（shell 還沒 mount），shell 會是 null：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 解法 1：等 DOMContentLoaded
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&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="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 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="c1">// 解法 2：MutationObserver 等 mount
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">bootstrap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&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">10&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">bootstrap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">disconnect&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="nx">bootstrap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>選擇取決於 shell 是 server-render 還是 client-render&lt;/strong>：server-render 用 DOMContentLoaded、client-render 用 observer。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&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;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>比本卡片簡潔、不防護未來變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：元件根變數&lt;/td>
 &lt;td>多一行設定、隔離未來頁面變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>比本卡片多支援多實例、設計成本前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>適合動態元件、不依賴變數綁定的時間&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>預設用本卡片、需要多實例升級到「起點當參數」、需要動態升級到「closest」。&lt;/p>
&lt;hr>
&lt;h2 id="應用範例完整-setup">應用範例：完整 setup&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">init&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 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 class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">input&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">drawer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 元件未完整 mount
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlotSwap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>shell 取一次、各 setup 函式從 shell 派生需要的子節點。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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">var</span> <span class="nx">shell</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-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</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="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</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">6</span><span class="cl"><span class="c1">// ... 之後所有 query 都從 shell 開始
</span></span></span></code></pre></div><p>把元件根 query 一次存變數、所有後續 query 都從這個變數開始。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把 selector 的作用範圍從「全頁面」收斂到「元件內部」。即使未來頁面其他地方出現同名元素、跟我無關。成本只多一行 query + 一個 null check、防護收益大。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production 客製、預期長期存活</td>
          <td>未來頁面結構可能變動、需要隔離</td>
      </tr>
      <tr>
          <td>當前只有一個元件實例、未來可能加</td>
          <td>提早預防、改造成本最低</td>
      </tr>
      <tr>
          <td>元件根 mount 後不會被移除</td>
          <td>變數生命週期跟元件一致</td>
      </tr>
      <tr>
          <td>程式跑在頁面 mount 後（DOMContentLoaded 後）</td>
          <td>shell 可被找到</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：寫的時候只有一個元件、但希望程式碼能容忍未來頁面結構變動。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁同時有多個元件實例</td>
          <td>變數只存第一個 shell、其他被忽略</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
      </tr>
      <tr>
          <td>元件動態增減（SPA 路由切換）</td>
          <td>變數指向 stale DOM</td>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
      </tr>
      <tr>
          <td>一次性 / 探索期程式</td>
          <td>過度工程</td>
          <td><a href="../pattern-document-query/">document query</a></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="null-check-的時機">Null check 的時機</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">var</span> <span class="nx">shell</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-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span></span></span></code></pre></div><p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會 null pointer。提早 return 比後續一連串 <code>if (drawer)</code> 乾淨。</p>
<p><strong>等同於宣告</strong>：「這段程式只在有 shell 的頁面執行」。</p>
<h3 id="變數的宣告位置">變數的宣告位置</h3>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>函式內 local 變數</td>
          <td>預設、scope 最小</td>
      </tr>
      <tr>
          <td>Module scope（IIFE 內）</td>
          <td>多函式共用同一 shell</td>
      </tr>
      <tr>
          <td>Class instance property</td>
          <td>元件本身用 class 包裝時</td>
      </tr>
  </tbody>
</table>
<p>避免全域變數 — <code>window.shell</code> 容易跟其他 script 撞。</p>
<h3 id="等待-shell-mount-的處理">等待 shell mount 的處理</h3>
<p>如果 script 跑得太早（shell 還沒 mount），shell 會是 null：</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">// 解法 1：等 DOMContentLoaded
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</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-shell&#39;</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="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 解法 2：MutationObserver 等 mount
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">bootstrap</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</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-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">bootstrap</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">bootstrap</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p><strong>選擇取決於 shell 是 server-render 還是 client-render</strong>：server-render 用 DOMContentLoaded、client-render 用 observer。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>比本卡片簡潔、不防護未來變動</td>
      </tr>
      <tr>
          <td>本卡片：元件根變數</td>
          <td>多一行設定、隔離未來頁面變動</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>比本卡片多支援多實例、設計成本前移</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>適合動態元件、不依賴變數綁定的時間</td>
      </tr>
  </tbody>
</table>
<p>預設用本卡片、需要多實例升級到「起點當參數」、需要動態升級到「closest」。</p>
<hr>
<h2 id="應用範例完整-setup">應用範例：完整 setup</h2>





<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">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</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-shell&#39;</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="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</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"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">input</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 元件未完整 mount
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</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">12</span><span class="cl">  <span class="nx">setupFilterSlotSwap</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">input</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="nx">init</span><span class="p">);</span></span></span></code></pre></div><p>shell 取一次、各 setup 函式從 shell 派生需要的子節點。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換做法嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多函式共用同一 shell、各自重 query</td>
          <td>否 — 把 shell 提到 module scope 共用</td>
      </tr>
      <tr>
          <td>同頁面要支援多個 shell 實例</td>
          <td>是 — 升級到「起點當參數」</td>
      </tr>
      <tr>
          <td>元件可能在 runtime 動態出現 / 消失</td>
          <td>是 — 升級到「closest 反向」</td>
      </tr>
      <tr>
          <td>Shell 偶爾找不到（時序問題）</td>
          <td>否 — 加 MutationObserver bootstrap、做法不變</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：本 pattern 是 production 客製的預設、不是極致最佳化。當當前情境不複雜（一個元件、靜態 mount）、用本 pattern 即可；情境變複雜時再升級到對應做法。</p>
]]></content:encoded></item><item><title>Pattern：起點當函式參數</title><link>https://tarrragon.github.io/blog/report/pattern-root-as-parameter/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-root-as-parameter/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&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="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 所有 query 從參數 shell 開始
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">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="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>元件根不在函式內 query、由呼叫者傳入。函式支援任意數量的元件實例。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>兩件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>多實例支援免費&lt;/strong>：&lt;code>forEach(setup)&lt;/code> 自動處理多個 shell&lt;/li>
&lt;li>&lt;strong>純函式特性&lt;/strong>：函式行為只依賴參數、不依賴外部狀態 — 可單獨測試、可重用、副作用集中&lt;/li>
&lt;/ol>
&lt;p>跟&lt;a href="../pattern-component-root/">元件根變數&lt;/a>的關鍵差異：那個 pattern 假設「shell 唯一」、本 pattern 把這個假設外移到呼叫端、函式本身不假設。&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>同頁面有多個元件實例（多語切換、相關搜尋）&lt;/td>
 &lt;td>&lt;code>forEach&lt;/code> 自動覆蓋全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件設計成可被重用到其他頁面&lt;/td>
 &lt;td>沒有 hardcoded 依賴、容易移植&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫成函式庫 / 第三方 component&lt;/td>
 &lt;td>使用者可以對任意根節點呼叫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>想單元測試函式行為&lt;/td>
 &lt;td>傳入 mock root 即可測試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：把「shell 從哪來」這個責任明確交給呼叫端、函式自己不關心。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>確定全站只有一個元件實例&lt;/td>
 &lt;td>每函式多一個參數、收益不明顯&lt;/td>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件動態增減、生命週期不可預測&lt;/td>
 &lt;td>forEach 只跑一次、無法捕捉後加的元件&lt;/td>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次性探索程式碼&lt;/td>
 &lt;td>純函式設計成本不值得&lt;/td>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="函式簽名的設計">函式簽名的設計&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 好：shell 是必填參數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 較差：依賴外部變數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// module scope
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 用了外部 shell
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// 更差：mega object
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">allElements&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">12&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">allElements&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 不知道實際依賴什麼
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>明確參數 = 明確依賴 = 容易測試、容易讀。&lt;/p>
&lt;h3 id="內部子函式也接受-shell">內部子函式也接受 shell&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&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 class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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"> 8&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每層都明確接受 shell — 不依賴外層 closure。整套函式族都是純函式。&lt;/p>
&lt;h3 id="預先抓子節點-vs-每次重-query">預先抓子節點 vs 每次重 query&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 方式 A：函式入口抓所有子節點
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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="kd">var&lt;/span> &lt;span class="nx">els&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">ui&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 後續用 els.ui / els.input / els.drawer
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">// 方式 B：各子函式自己 query
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&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">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 內部自己 querySelector
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">setupFilterSlot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A 比較有效率（只 query 一次）、B 比較解耦（子函式自包含）。&lt;strong>選 B 為預設、效能瓶頸時才考慮 A&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&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;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>比本卡片簡潔、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>比本卡片少一個參數、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：起點當參數&lt;/td>
 &lt;td>多實例支援、純函式、設計成本前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>比本卡片更動態、不依賴 forEach 時機&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>升級階梯：document → 元件根變數 → 起點當參數 → closest。複雜度遞增、能處理的情境也遞增。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</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">5</span><span class="cl">  <span class="c1">// ... 所有 query 從參數 shell 開始
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span></span></span></code></pre></div><p>元件根不在函式內 query、由呼叫者傳入。函式支援任意數量的元件實例。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>兩件事：</p>
<ol>
<li><strong>多實例支援免費</strong>：<code>forEach(setup)</code> 自動處理多個 shell</li>
<li><strong>純函式特性</strong>：函式行為只依賴參數、不依賴外部狀態 — 可單獨測試、可重用、副作用集中</li>
</ol>
<p>跟<a href="../pattern-component-root/">元件根變數</a>的關鍵差異：那個 pattern 假設「shell 唯一」、本 pattern 把這個假設外移到呼叫端、函式本身不假設。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁面有多個元件實例（多語切換、相關搜尋）</td>
          <td><code>forEach</code> 自動覆蓋全部</td>
      </tr>
      <tr>
          <td>元件設計成可被重用到其他頁面</td>
          <td>沒有 hardcoded 依賴、容易移植</td>
      </tr>
      <tr>
          <td>寫成函式庫 / 第三方 component</td>
          <td>使用者可以對任意根節點呼叫</td>
      </tr>
      <tr>
          <td>想單元測試函式行為</td>
          <td>傳入 mock root 即可測試</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：把「shell 從哪來」這個責任明確交給呼叫端、函式自己不關心。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼過度工程</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>確定全站只有一個元件實例</td>
          <td>每函式多一個參數、收益不明顯</td>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
      </tr>
      <tr>
          <td>元件動態增減、生命週期不可預測</td>
          <td>forEach 只跑一次、無法捕捉後加的元件</td>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
      </tr>
      <tr>
          <td>一次性探索程式碼</td>
          <td>純函式設計成本不值得</td>
          <td><a href="../pattern-document-query/">document query</a></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="函式簽名的設計">函式簽名的設計</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="c1">// 好：shell 是必填參數
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</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="c1">// 較差：依賴外部變數
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">shell</span><span class="p">;</span>  <span class="c1">// module scope
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 用了外部 shell
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><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">// 更差：mega object
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">allElements</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">allElements</span><span class="p">.</span><span class="nx">shell</span><span class="p">;</span>  <span class="c1">// 不知道實際依賴什麼
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>明確參數 = 明確依賴 = 容易測試、容易讀。</p>
<h3 id="內部子函式也接受-shell">內部子函式也接受 shell</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">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">setupFilterSlot</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">shell</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></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">shell</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"> 9</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>每層都明確接受 shell — 不依賴外層 closure。整套函式族都是純函式。</p>
<h3 id="預先抓子節點-vs-每次重-query">預先抓子節點 vs 每次重 query</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="c1">// 方式 A：函式入口抓所有子節點
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">els</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">ui</span><span class="o">:</span>     <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">input</span><span class="o">:</span>  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="o">:</span> <span class="nx">shell</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"> 7</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// 後續用 els.ui / els.input / els.drawer
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// 方式 B：各子函式自己 query
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>  <span class="c1">// 內部自己 querySelector
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="nx">setupFilterSlot</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>A 比較有效率（只 query 一次）、B 比較解耦（子函式自包含）。<strong>選 B 為預設、效能瓶頸時才考慮 A</strong>。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>比本卡片簡潔、無多實例支援</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>比本卡片少一個參數、無多實例支援</td>
      </tr>
      <tr>
          <td>本卡片：起點當參數</td>
          <td>多實例支援、純函式、設計成本前移</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>比本卡片更動態、不依賴 forEach 時機</td>
      </tr>
  </tbody>
</table>
<p>升級階梯：document → 元件根變數 → 起點當參數 → closest。複雜度遞增、能處理的情境也遞增。</p>
<hr>
<h2 id="應用範例多實例-setup">應用範例：多實例 setup</h2>





<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">// 頁面有 N 個 search-shell（例如多語版面切換）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</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="c1">// 跑完之後：每個 shell 各自獨立 setup、互不干擾
</span></span></span></code></pre></div><p>當前頁只一個 shell、上面這行也適用 —<code>forEach</code> 對 1 個元素跑一次、跟 hardcode 單例沒差。<strong>做了多實例設計、未來不需要重寫</strong>。</p>
<hr>
<h2 id="應用範例單元測試">應用範例：單元測試</h2>
<p>純函式可以對 mock DOM 測試：</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="nx">test</span><span class="p">(</span><span class="s1">&#39;setupSearchShell 把 filter 移到 sidebar&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">createMockShell</span><span class="p">();</span>  <span class="c1">// 建立測試用 DOM
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">).</span><span class="nx">children</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</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>不需要全頁面 mount、只需要 mock 一個 shell — 測試成本低。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁要支援多個元件實例</td>
          <td>是 — 直接的好處</td>
      </tr>
      <tr>
          <td>想對函式寫單元測試</td>
          <td>是 — 純函式才好測</td>
      </tr>
      <tr>
          <td>函式內讀 module scope 變數</td>
          <td>是 — 改成參數讓依賴顯式</td>
      </tr>
      <tr>
          <td>確定永遠只一個實例、且不寫測試</td>
          <td>否 — <a href="../pattern-component-root/">元件根變數</a> 已夠</td>
      </tr>
      <tr>
          <td>元件實例 runtime 動態增減</td>
          <td>否 — 升級到 <a href="../pattern-closest-lookup/">closest</a></td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：本 pattern 把「我從哪取得 shell」的答案從函式內搬到呼叫端 — 換到「函式可重用」+「測試容易」+「多實例免費」三個收益、代價是函式簽名多一個參數。當前情境只一個實例也適用、未來擴展不需重寫。</p>
]]></content:encoded></item><item><title>Pattern：closest 反向找根</title><link>https://tarrragon.github.io/blog/report/pattern-closest-lookup/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-closest-lookup/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 在這個 shell 內處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">handleSearchClick&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不在初始化時綁定 listener、而是頁面層級委派事件、事件處理時從 &lt;code>e.target&lt;/code> 反向找元件根。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把「找元件根」從「初始化時綁定」延後到「事件發生時動態判斷」 — 換到三個能力：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>元件動態增減免處理&lt;/strong>：新加的元件不需要重新綁 listener&lt;/li>
&lt;li>&lt;strong>多實例不需要 forEach setup&lt;/strong>：所有實例共用一個 listener&lt;/li>
&lt;li>&lt;strong>記憶體效率&lt;/strong>：N 個元件只綁 1 個 listener、不是 N 個&lt;/li>
&lt;/ol>
&lt;p>代價是事件處理邏輯多一層（每次都要 closest 反向找）。&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>SPA 路由切換、元件動態 mount/unmount&lt;/td>
 &lt;td>不需要在 mount 時重綁 listener&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件數量大（&amp;gt;10 個實例）&lt;/td>
 &lt;td>事件委派比每實例綁 listener 省記憶體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件透過 AJAX 動態注入&lt;/td>
 &lt;td>注入後不需要任何 setup 動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方 widget、不能控制元件生命週期&lt;/td>
 &lt;td>listener 綁在 document、跟 widget 解耦&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：元件的 mount 時機 / 數量 runtime 才知道、不是初始化時固定。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>元件靜態 mount、生命週期跟頁面一樣&lt;/td>
 &lt;td>委派多一層、收益不明顯&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個元件實例、永不變動&lt;/td>
 &lt;td>完全沒必要&lt;/td>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要在元件 mount 時就跑邏輯（不只回應事件）&lt;/td>
 &lt;td>closest 只在事件發生時跑、無法當 init hook&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a> + MutationObserver&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="closest-失敗的處理">Closest 失敗的處理&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 點擊不在任何 shell 內
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>closest&lt;/code> 找不到時回 &lt;code>null&lt;/code>、提早 return 是必要防護。&lt;strong>沒這個 check 會在頁面其他地方點擊時報錯&lt;/strong>。&lt;/p>
&lt;h3 id="從-closest-結果再往下-query">從 closest 結果再往下 query&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>closest&lt;/code> 找到 shell 後、可以從 shell 往下 query 同元件內的其他元素 — 這是「事件 + closest + 局部 query」的組合。&lt;/p>
&lt;h3 id="事件類型的選擇">事件類型的選擇&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>事件&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>click&lt;/code>&lt;/td>
 &lt;td>點擊互動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>input&lt;/code>&lt;/td>
 &lt;td>輸入框文字變動（需要 bubble）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>change&lt;/code>&lt;/td>
 &lt;td>選項變動（select / radio / checkbox）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>keydown&lt;/code>&lt;/td>
 &lt;td>鍵盤快捷鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>focus&lt;/code> / &lt;code>blur&lt;/code>&lt;/td>
 &lt;td>焦點移動（不 bubble、要用 &lt;code>focusin&lt;/code> / &lt;code>focusout&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>注意 &lt;code>focus&lt;/code> / &lt;code>blur&lt;/code> 不會 bubble — 事件委派要用 &lt;code>focusin&lt;/code> / &lt;code>focusout&lt;/code>。&lt;/p>
&lt;h3 id="委派的根節點選擇">委派的根節點選擇&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 選項 1：document（最寬）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handler&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>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 選項 2：特定容器（縮範圍）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">pageContainer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;main&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">pageContainer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handler&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>縮範圍的好處是「跟其他頁面區域的 listener 不互相干擾」。預設用 document、有干擾風險才縮。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&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;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>靜態、簡潔、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>靜態、shell 唯一假設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>靜態多實例、forEach 一次設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：closest 反向找根&lt;/td>
 &lt;td>動態、事件驅動、無 init 時機綁定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>複雜度遞增、能處理的動態程度也遞增。最動態的場景才用本 pattern。&lt;/p>
&lt;hr>
&lt;h2 id="應用範例跨多-shell-的-scope-filter">應用範例：跨多 shell 的 scope filter&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupGlobalScopeFilter&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&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="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 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 class="kd">var&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">scope&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 不是 scope 控制的 change
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">applyScope&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scope&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nx">setupGlobalScopeFilter&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一個 listener 處理所有 shell 的 scope 變動 — 不論 shell 是初始 mount 的、還是 runtime 注入的。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</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="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="c1">// 在這個 shell 內處理
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">handleSearchClick</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">e</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>不在初始化時綁定 listener、而是頁面層級委派事件、事件處理時從 <code>e.target</code> 反向找元件根。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把「找元件根」從「初始化時綁定」延後到「事件發生時動態判斷」 — 換到三個能力：</p>
<ol>
<li><strong>元件動態增減免處理</strong>：新加的元件不需要重新綁 listener</li>
<li><strong>多實例不需要 forEach setup</strong>：所有實例共用一個 listener</li>
<li><strong>記憶體效率</strong>：N 個元件只綁 1 個 listener、不是 N 個</li>
</ol>
<p>代價是事件處理邏輯多一層（每次都要 closest 反向找）。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SPA 路由切換、元件動態 mount/unmount</td>
          <td>不需要在 mount 時重綁 listener</td>
      </tr>
      <tr>
          <td>元件數量大（&gt;10 個實例）</td>
          <td>事件委派比每實例綁 listener 省記憶體</td>
      </tr>
      <tr>
          <td>元件透過 AJAX 動態注入</td>
          <td>注入後不需要任何 setup 動作</td>
      </tr>
      <tr>
          <td>第三方 widget、不能控制元件生命週期</td>
          <td>listener 綁在 document、跟 widget 解耦</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：元件的 mount 時機 / 數量 runtime 才知道、不是初始化時固定。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼過度工程</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件靜態 mount、生命週期跟頁面一樣</td>
          <td>委派多一層、收益不明顯</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
      </tr>
      <tr>
          <td>一個元件實例、永不變動</td>
          <td>完全沒必要</td>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
      </tr>
      <tr>
          <td>需要在元件 mount 時就跑邏輯（不只回應事件）</td>
          <td>closest 只在事件發生時跑、無法當 init hook</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a> + MutationObserver</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="closest-失敗的處理">Closest 失敗的處理</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">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</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="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 點擊不在任何 shell 內
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p><code>closest</code> 找不到時回 <code>null</code>、提早 return 是必要防護。<strong>沒這個 check 會在頁面其他地方點擊時報錯</strong>。</p>
<h3 id="從-closest-結果再往下-query">從 closest 結果再往下 query</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">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">var</span> <span class="nx">input</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>closest</code> 找到 shell 後、可以從 shell 往下 query 同元件內的其他元素 — 這是「事件 + closest + 局部 query」的組合。</p>
<h3 id="事件類型的選擇">事件類型的選擇</h3>
<table>
  <thead>
      <tr>
          <th>事件</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>click</code></td>
          <td>點擊互動</td>
      </tr>
      <tr>
          <td><code>input</code></td>
          <td>輸入框文字變動（需要 bubble）</td>
      </tr>
      <tr>
          <td><code>change</code></td>
          <td>選項變動（select / radio / checkbox）</td>
      </tr>
      <tr>
          <td><code>keydown</code></td>
          <td>鍵盤快捷鍵</td>
      </tr>
      <tr>
          <td><code>focus</code> / <code>blur</code></td>
          <td>焦點移動（不 bubble、要用 <code>focusin</code> / <code>focusout</code>）</td>
      </tr>
  </tbody>
</table>
<p>注意 <code>focus</code> / <code>blur</code> 不會 bubble — 事件委派要用 <code>focusin</code> / <code>focusout</code>。</p>
<h3 id="委派的根節點選擇">委派的根節點選擇</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="c1">// 選項 1：document（最寬）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">handler</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="c1">// 選項 2：特定容器（縮範圍）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">pageContainer</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;main&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">pageContainer</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">handler</span><span class="p">);</span></span></span></code></pre></div><p>縮範圍的好處是「跟其他頁面區域的 listener 不互相干擾」。預設用 document、有干擾風險才縮。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>靜態、簡潔、無多實例支援</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>靜態、shell 唯一假設</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>靜態多實例、forEach 一次設定</td>
      </tr>
      <tr>
          <td>本卡片：closest 反向找根</td>
          <td>動態、事件驅動、無 init 時機綁定</td>
      </tr>
  </tbody>
</table>
<p>複雜度遞增、能處理的動態程度也遞增。最動態的場景才用本 pattern。</p>
<hr>
<h2 id="應用範例跨多-shell-的-scope-filter">應用範例：跨多 shell 的 scope filter</h2>





<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">setupGlobalScopeFilter</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</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="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</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"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">scope</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 不是 scope 控制的 change
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">applyScope</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">scope</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">setupGlobalScopeFilter</span><span class="p">();</span></span></span></code></pre></div><p>一個 listener 處理所有 shell 的 scope 變動 — 不論 shell 是初始 mount 的、還是 runtime 注入的。</p>
<hr>
<h2 id="應用範例與-起點當參數-組合">應用範例：與 <a href="../pattern-root-as-parameter/">起點當參數</a> 組合</h2>





<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">// 初始化階段：對已存在的 shell 做 setup
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</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="c1">// 事件階段：用 closest 處理可能新加的 shell
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// 處理事件、不論 shell 是初始的還是後加的
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// MutationObserver：捕捉新加的 shell 做 setup
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">mutations</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">mutations</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">m</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">addedNodes</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">node</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">matches</span> <span class="o">&amp;&amp;</span> <span class="nx">node</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">node</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>三個 pattern 組合：「靜態 setup」+「事件動態」+「mount 時 setup」 — 各 pattern 補不同時間點的需求。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件 SPA 路由動態切換</td>
          <td>是 — 直接對應使用情境</td>
      </tr>
      <tr>
          <td>元件數量大、每實例都要綁 listener</td>
          <td>是 — 委派省記憶體</td>
      </tr>
      <tr>
          <td>AJAX / Web Component runtime 注入</td>
          <td>是 — 不需要重綁</td>
      </tr>
      <tr>
          <td>確定元件靜態、生命週期固定</td>
          <td>否 — <a href="../pattern-root-as-parameter/">起點當參數</a> 已夠</td>
      </tr>
      <tr>
          <td>邏輯不是事件驅動（init 時就要跑）</td>
          <td>否 — closest 只在事件發生時跑</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：closest 反向找根把「定位元件」從綁定時延後到事件發生時 — 換到動態能力、付出的是事件處理多一層判斷。靜態場景用更簡單的做法、動態場景才升級到本 pattern。</p>
]]></content:encoded></item><item><title>Pattern：DOM attribute idempotency 標記</title><link>https://tarrragon.github.io/blog/report/pattern-attribute-idempotency-marker/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-attribute-idempotency-marker/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result:not([data-scoped])&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&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">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&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>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>apply 函式入口用 &lt;code>:not([data-x])&lt;/code> 過濾掉已處理元素、處理完後設 attribute 標記。下次 apply 被觸發時、已處理的元素不會被命中。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把「保證只處理一次」的責任從&lt;strong>呼叫端&lt;/strong>（要記得只呼叫一次）轉到&lt;strong>元素本身&lt;/strong>（看自己有沒有被處理過）。&lt;/p>
&lt;p>apply 函式可能被多個源觸發：&lt;/p>
&lt;ul>
&lt;li>初始化時呼叫&lt;/li>
&lt;li>MutationObserver 偵測到變動觸發&lt;/li>
&lt;li>使用者事件觸發&lt;/li>
&lt;li>Framework 重繪後重新呼叫&lt;/li>
&lt;/ul>
&lt;p>任一個源多呼叫就重複處理 — 無法靠呼叫端紀律避免。Idempotency 標記讓 apply 自己防護。&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>Production apply 函式、可能被多源觸發&lt;/td>
 &lt;td>標記在元素上、不依賴呼叫紀律&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>處理動作有副作用（綁 listener、改 class）&lt;/td>
 &lt;td>重複觸發會疊加副作用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元素生命週期跟 attribute 同步（不會被 reset）&lt;/td>
 &lt;td>標記跟著元素走、自然清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Devtools debug 友善&lt;/td>
 &lt;td>attribute 在 inspector 可見&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：元素的 attribute 跟著元素 DOM 生命週期、元素移除時標記自動消失。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫第三方 library&lt;/td>
 &lt;td>在使用者 DOM 加自家 attribute、有命名衝突風險&lt;/td>
 &lt;td>&lt;a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 重繪會清掉 attribute&lt;/td>
 &lt;td>標記消失、防護失效&lt;/td>
 &lt;td>配合 disconnect/observe 或改 WeakMap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要週期性 reset 標記&lt;/td>
 &lt;td>attribute 改回需要遍歷所有元素&lt;/td>
 &lt;td>WeakMap 可整批 &lt;code>new WeakMap()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多種獨立的 idempotency 維度&lt;/td>
 &lt;td>DOM 上多 attribute 互相干擾&lt;/td>
 &lt;td>WeakMap 各別管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="attribute-命名規範">Attribute 命名規範&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 好：明確 namespace + 用途
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-search-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-myapp-processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 較差：通用名、容易跟其他程式撞
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 不是 data-* 開頭、可能不被 HTML spec 接受
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設用 &lt;code>data-{appname}-{purpose}&lt;/code> 格式 — 即使引入第三方 library 加 attribute、也不會撞名。&lt;/p>
&lt;h3 id="attribute-值的選擇">Attribute 值的選擇&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 1：固定 &amp;#39;true&amp;#39;（最簡）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 2：紀錄處理時間 / 版本（debug 友善）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;v2&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 3：boolean attribute（無值）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&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="c1">// CSS 用 [data-scoped] 即可選中
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設用 &lt;code>'true'&lt;/code>、debug 困難時改 timestamp 看處理順序。&lt;/p>
&lt;h3 id="跟-framework-重繪共處">跟 framework 重繪共處&lt;/h3>
&lt;p>Svelte / React / Vue 重繪元素時、&lt;strong>自家 attribute 通常會被保留&lt;/strong>（framework 只管自己的 attribute）— 但有例外：&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>Framework re-render 整段 DOM&lt;/td>
 &lt;td>元素被替換、新元素沒標記 → apply 重跑、合理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework patch 既有元素 attribute&lt;/td>
 &lt;td>自家 attribute 保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework &lt;code>replaceWith&lt;/code> / &lt;code>innerHTML&lt;/code> 重設&lt;/td>
 &lt;td>元素被替換 → 標記消失、apply 重跑、合理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心觀察&lt;/strong>：自家 attribute 跟著元素走 — 元素還在就有、元素被換就沒。這是「正確」行為、不是 bug。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result:not([data-scoped])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>apply 函式入口用 <code>:not([data-x])</code> 過濾掉已處理元素、處理完後設 attribute 標記。下次 apply 被觸發時、已處理的元素不會被命中。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把「保證只處理一次」的責任從<strong>呼叫端</strong>（要記得只呼叫一次）轉到<strong>元素本身</strong>（看自己有沒有被處理過）。</p>
<p>apply 函式可能被多個源觸發：</p>
<ul>
<li>初始化時呼叫</li>
<li>MutationObserver 偵測到變動觸發</li>
<li>使用者事件觸發</li>
<li>Framework 重繪後重新呼叫</li>
</ul>
<p>任一個源多呼叫就重複處理 — 無法靠呼叫端紀律避免。Idempotency 標記讓 apply 自己防護。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production apply 函式、可能被多源觸發</td>
          <td>標記在元素上、不依賴呼叫紀律</td>
      </tr>
      <tr>
          <td>處理動作有副作用（綁 listener、改 class）</td>
          <td>重複觸發會疊加副作用</td>
      </tr>
      <tr>
          <td>元素生命週期跟 attribute 同步（不會被 reset）</td>
          <td>標記跟著元素走、自然清理</td>
      </tr>
      <tr>
          <td>Devtools debug 友善</td>
          <td>attribute 在 inspector 可見</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：元素的 attribute 跟著元素 DOM 生命週期、元素移除時標記自動消失。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library</td>
          <td>在使用者 DOM 加自家 attribute、有命名衝突風險</td>
          <td><a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></td>
      </tr>
      <tr>
          <td>Framework 重繪會清掉 attribute</td>
          <td>標記消失、防護失效</td>
          <td>配合 disconnect/observe 或改 WeakMap</td>
      </tr>
      <tr>
          <td>需要週期性 reset 標記</td>
          <td>attribute 改回需要遍歷所有元素</td>
          <td>WeakMap 可整批 <code>new WeakMap()</code></td>
      </tr>
      <tr>
          <td>多種獨立的 idempotency 維度</td>
          <td>DOM 上多 attribute 互相干擾</td>
          <td>WeakMap 各別管理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="attribute-命名規範">Attribute 命名規範</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="c1">// 好：明確 namespace + 用途
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-myapp-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 較差：通用名、容易跟其他程式撞
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>  <span class="c1">// 不是 data-* 開頭、可能不被 HTML spec 接受
</span></span></span></code></pre></div><p>預設用 <code>data-{appname}-{purpose}</code> 格式 — 即使引入第三方 library 加 attribute、也不會撞名。</p>
<h3 id="attribute-值的選擇">Attribute 值的選擇</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="c1">// 用法 1：固定 &#39;true&#39;（最簡）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</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="c1">// 用法 2：紀錄處理時間 / 版本（debug 友善）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="nb">String</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()));</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;v2&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 用法 3：boolean attribute（無值）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// CSS 用 [data-scoped] 即可選中
</span></span></span></code></pre></div><p>預設用 <code>'true'</code>、debug 困難時改 timestamp 看處理順序。</p>
<h3 id="跟-framework-重繪共處">跟 framework 重繪共處</h3>
<p>Svelte / React / Vue 重繪元素時、<strong>自家 attribute 通常會被保留</strong>（framework 只管自己的 attribute）— 但有例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Framework re-render 整段 DOM</td>
          <td>元素被替換、新元素沒標記 → apply 重跑、合理</td>
      </tr>
      <tr>
          <td>Framework patch 既有元素 attribute</td>
          <td>自家 attribute 保留</td>
      </tr>
      <tr>
          <td>Framework <code>replaceWith</code> / <code>innerHTML</code> 重設</td>
          <td>元素被替換 → 標記消失、apply 重跑、合理</td>
      </tr>
  </tbody>
</table>
<p><strong>核心觀察</strong>：自家 attribute 跟著元素走 — 元素還在就有、元素被換就沒。這是「正確」行為、不是 bug。</p>
<h3 id="例外framework-主動清自家-attribute">例外：framework 主動清自家 attribute</h3>
<p>少數 framework 會 strict 清非預期的 attribute（例如某些 Web Component lib）。檢查方式：</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="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// ... 等 framework patch 一次後
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">));</span>  <span class="c1">// 還在嗎？
</span></span></span></code></pre></div><p>如果消失、改用 <a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a>。</p>
<hr>
<h2 id="跟其他-idempotency-做法的關係">跟其他 idempotency 做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「過濾」維度有三種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本卡片：DOM attribute 標記</td>
          <td>production 預設、devtools 可見、有命名衝突風險</td>
      </tr>
      <tr>
          <td><a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></td>
          <td>不污染 DOM、適合 library、debug 不便</td>
      </tr>
      <tr>
          <td>依賴外部呼叫者保證</td>
          <td>反模式、無防護、不可靠</td>
      </tr>
  </tbody>
</table>
<p>預設用本卡片、第三方 library / framework 衝突情境升級到 WeakMap。</p>
<hr>
<h2 id="應用範例完整-apply">應用範例：完整 apply</h2>





<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">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">newResults</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s1">&#39;.pagefind-ui__result:not([data-search-scoped])&#39;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nx">newResults</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">bindClickHandler</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">addCustomBadge</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// 多源觸發都安全
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="nx">init</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">...);</span>  <span class="c1">// 觀察到變動觸發 apply
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>  <span class="c1">// 初始化時跑一次
</span></span></span></code></pre></div><p>三個觸發點任一個多跑、<code>:not([data-search-scoped])</code> 都會過濾掉已處理元素。</p>
<hr>
<h2 id="應用範例多維度標記">應用範例：多維度標記</h2>





<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">// 三個獨立 idempotency 維度、各自 attribute
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>     <span class="c1">// scope filter 處理過
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-bound&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>      <span class="c1">// event listener 綁過
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-decorated&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</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></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 各 apply 函式只看自己的 attribute
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">applyScope</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x:not([data-search-scoped])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(...)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">function</span> <span class="nx">applyBindings</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x:not([data-search-bound])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(...)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>每個 idempotency 維度獨立 — 互相不干擾。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Apply 被多源觸發、產生重複處理 bug</td>
          <td>是 — 直接對應使用情境</td>
      </tr>
      <tr>
          <td>寫第三方 library / 不能污染 DOM</td>
          <td>否 — 改 <a href="../pattern-weakmap-idempotency-record/">WeakMap</a></td>
      </tr>
      <tr>
          <td>Framework 會清自家 attribute</td>
          <td>否 — 改 WeakMap</td>
      </tr>
      <tr>
          <td>想在 devtools inspector 直接看處理狀態</td>
          <td>是 — attribute 可見性是優點</td>
      </tr>
      <tr>
          <td>同元素多種 idempotency 維度</td>
          <td>是 — 多 attribute 各自管理</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：把 idempotency 責任從呼叫端搬到元素本身、attribute 是「便宜可見的旗標」。Production apply 預設用本 pattern、特殊情境（library / framework 衝突）才升級到 WeakMap。</p>
]]></content:encoded></item><item><title>Pattern：WeakMap idempotency 紀錄</title><link>https://tarrragon.github.io/blog/report/pattern-weakmap-idempotency-record/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-weakmap-idempotency-record/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">processed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">WeakMap&lt;/span>&lt;span class="p">();&lt;/span>
&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 class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">has&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把「已處理」狀態紀錄在 JS 的 WeakMap 裡、不寫到 DOM 上。WeakMap key 是元素本身、元素被 GC 時自動清理。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>兩件事 &lt;a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記&lt;/a> 做不到：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>不污染 DOM&lt;/strong>：使用者 DOM 不會被加自家 attribute、適合第三方 library&lt;/li>
&lt;li>&lt;strong>跟 framework 完全解耦&lt;/strong>：framework 怎麼操作 DOM 都不影響 WeakMap 紀錄&lt;/li>
&lt;/ol>
&lt;p>代價是 debug 不便（看不到狀態）、紀錄跟 JS context 綁定（換頁就消失）。&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>寫第三方 library / npm package&lt;/td>
 &lt;td>不在使用者 DOM 加 attribute、避免命名衝突&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 會清非預期的 attribute&lt;/td>
 &lt;td>WeakMap 不在 DOM、framework 動不到&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要週期性 reset 紀錄&lt;/td>
 &lt;td>&lt;code>processed = new WeakMap()&lt;/code> 一行重置全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>紀錄複雜資料、不只是 boolean&lt;/td>
 &lt;td>WeakMap value 可以是任何物件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：紀錄獨立於 DOM 之外、跟 JS 物件 lifetime 綁定。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>自家 application、devtools debug 重要&lt;/td>
 &lt;td>看不到狀態、debug 困難&lt;/td>
 &lt;td>&lt;a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨頁面 / 跨 session 的 idempotency&lt;/td>
 &lt;td>WeakMap 在 JS context 內、換頁就消失&lt;/td>
 &lt;td>LocalStorage / 後端紀錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元素生命週期短、頻繁 GC&lt;/td>
 &lt;td>WeakMap 自動清理可能比預期早&lt;/td>
 &lt;td>改用 Map（但要手動清理）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>紀錄要跟 SSR 同步&lt;/td>
 &lt;td>WeakMap 只活在 client&lt;/td>
 &lt;td>結合 attribute（SSR 階段標記）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="為什麼用-weakmap-不用-map--set">為什麼用 WeakMap 不用 Map / Set&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// WeakMap：key 是元素、元素被 GC 時 entry 自動消失
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">processedW&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">WeakMap&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">processedW&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&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="c1">// el 從 DOM 移除 + 沒其他 reference → GC → WeakMap entry 消失
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">// Map / Set：強引用、阻止 GC
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">processedS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Set&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nx">processedS&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1">// el 從 DOM 移除、但 Set 還抓著 → 永久 leak
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM 元素可能動態移除（filter、SPA 路由切換、framework 重繪）— Map / Set 會造成 memory leak。&lt;strong>處理 DOM 元素 idempotency 預設用 WeakMap&lt;/strong>。&lt;/p>
&lt;h3 id="value-的設計">Value 的設計&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 1：純 boolean（最簡）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 2：紀錄處理版本（升級時偵測 stale 紀錄）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">version&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&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"> 6&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">has&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">version&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">currentVersion&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 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="c1">// 用法 3：紀錄相關 metadata（避免重複查詢）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&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">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">bindingsId&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">registerListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&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="nx">initialClass&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="p">,&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>WeakMap value 可以儲任何資料 — 比 attribute（只能存字串）更彈性。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</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="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</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">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>把「已處理」狀態紀錄在 JS 的 WeakMap 裡、不寫到 DOM 上。WeakMap key 是元素本身、元素被 GC 時自動清理。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>兩件事 <a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a> 做不到：</p>
<ol>
<li><strong>不污染 DOM</strong>：使用者 DOM 不會被加自家 attribute、適合第三方 library</li>
<li><strong>跟 framework 完全解耦</strong>：framework 怎麼操作 DOM 都不影響 WeakMap 紀錄</li>
</ol>
<p>代價是 debug 不便（看不到狀態）、紀錄跟 JS context 綁定（換頁就消失）。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library / npm package</td>
          <td>不在使用者 DOM 加 attribute、避免命名衝突</td>
      </tr>
      <tr>
          <td>Framework 會清非預期的 attribute</td>
          <td>WeakMap 不在 DOM、framework 動不到</td>
      </tr>
      <tr>
          <td>需要週期性 reset 紀錄</td>
          <td><code>processed = new WeakMap()</code> 一行重置全部</td>
      </tr>
      <tr>
          <td>紀錄複雜資料、不只是 boolean</td>
          <td>WeakMap value 可以是任何物件</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：紀錄獨立於 DOM 之外、跟 JS 物件 lifetime 綁定。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自家 application、devtools debug 重要</td>
          <td>看不到狀態、debug 困難</td>
          <td><a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a></td>
      </tr>
      <tr>
          <td>跨頁面 / 跨 session 的 idempotency</td>
          <td>WeakMap 在 JS context 內、換頁就消失</td>
          <td>LocalStorage / 後端紀錄</td>
      </tr>
      <tr>
          <td>元素生命週期短、頻繁 GC</td>
          <td>WeakMap 自動清理可能比預期早</td>
          <td>改用 Map（但要手動清理）</td>
      </tr>
      <tr>
          <td>紀錄要跟 SSR 同步</td>
          <td>WeakMap 只活在 client</td>
          <td>結合 attribute（SSR 階段標記）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="為什麼用-weakmap-不用-map--set">為什麼用 WeakMap 不用 Map / Set</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="c1">// WeakMap：key 是元素、元素被 GC 時 entry 自動消失
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">processedW</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">processedW</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// el 從 DOM 移除 + 沒其他 reference → GC → WeakMap entry 消失
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// Map / Set：強引用、阻止 GC
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">processedS</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nx">processedS</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">// el 從 DOM 移除、但 Set 還抓著 → 永久 leak
</span></span></span></code></pre></div><p>DOM 元素可能動態移除（filter、SPA 路由切換、framework 重繪）— Map / Set 會造成 memory leak。<strong>處理 DOM 元素 idempotency 預設用 WeakMap</strong>。</p>
<h3 id="value-的設計">Value 的設計</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="c1">// 用法 1：純 boolean（最簡）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</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="c1">// 用法 2：紀錄處理版本（升級時偵測 stale 紀錄）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span> <span class="nx">version</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nx">time</span><span class="o">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">processed</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">el</span><span class="p">).</span><span class="nx">version</span> <span class="o">===</span> <span class="nx">currentVersion</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 用法 3：紀錄相關 metadata（避免重複查詢）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nx">bindingsId</span><span class="o">:</span> <span class="nx">registerListener</span><span class="p">(</span><span class="nx">el</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">initialClass</span><span class="o">:</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>WeakMap value 可以儲任何資料 — 比 attribute（只能存字串）更彈性。</p>
<h3 id="debug-替代方案">Debug 替代方案</h3>
<p>attribute 標記可以在 devtools inspector 直接看；WeakMap 看不到。debug 時的替代：</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">// 開發模式同步寫一份 attribute（production build 時拿掉）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">markProcessed</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">DEV_MODE</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-debug-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</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 class="p">}</span></span></span></code></pre></div><p>或暴露到 console：</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="nb">window</span><span class="p">.</span><span class="nx">__debug_processed</span> <span class="o">=</span> <span class="nx">processed</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// console: __debug_processed.has($0)  // 檢查當前選中元素
</span></span></span></code></pre></div><p>這些都是 workaround、不如 attribute 標記直觀。<strong>選 WeakMap 的人通常已經接受這個 debug 成本</strong>。</p>
<h3 id="reset-紀錄">Reset 紀錄</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="c1">// WeakMap 整批 reset
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</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="c1">// 對比 attribute 整批 reset 要遍歷
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;[data-scoped]&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">el</span><span class="p">.</span><span class="nx">removeAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>需要週期性 reset（例如 user 切換 mode、所有元素該重新處理）— WeakMap 一行解決、attribute 要遍歷。</p>
<hr>
<h2 id="跟其他-idempotency-做法的關係">跟其他 idempotency 做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「過濾」維度有三種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a></td>
          <td>production 預設、devtools 可見、有命名衝突風險</td>
      </tr>
      <tr>
          <td>本卡片：WeakMap 紀錄</td>
          <td>不污染 DOM、適合 library、debug 不便</td>
      </tr>
      <tr>
          <td>依賴外部呼叫者保證</td>
          <td>反模式、無防護</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>自家 application</strong> → attribute；<strong>library / framework 衝突</strong> → WeakMap；<strong>反模式不選</strong>。</p>
<hr>
<h2 id="應用範例library-設計">應用範例：library 設計</h2>





<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">// 第三方 library export 的 init 函式
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">initSearchEnhancement</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nx">enhanceResult</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">apply</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">// 使用者：
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="nx">initSearchEnhancement</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.my-search&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1">// 不會在使用者 DOM 上加任何 data-* attribute
</span></span></span></code></pre></div><p>使用者 DOM 完全乾淨、library 行為內聚。</p>
<hr>
<h2 id="應用範例版本化處理">應用範例：版本化處理</h2>





<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">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">var</span> <span class="nx">CURRENT_VERSION</span> <span class="o">=</span> <span class="mi">3</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="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">record</span> <span class="o">=</span> <span class="nx">processed</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">record</span> <span class="o">&amp;&amp;</span> <span class="nx">record</span><span class="p">.</span><span class="nx">version</span> <span class="o">===</span> <span class="nx">CURRENT_VERSION</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1">// 升級到新版本（可能需要清舊綁定）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">record</span><span class="p">)</span> <span class="nx">cleanup</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="nx">record</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">enhance</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="nx">CURRENT_VERSION</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span> <span class="nx">version</span><span class="o">:</span> <span class="nx">CURRENT_VERSION</span><span class="p">,</span> <span class="nx">time</span><span class="o">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>版本變動時 — 不需要遍歷 DOM 清舊 attribute、直接用 WeakMap value 比對。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library / npm package</td>
          <td>是 — 不污染使用者 DOM</td>
      </tr>
      <tr>
          <td>Framework 會 strict 清自家 attribute</td>
          <td>是 — WeakMap 跟 framework 解耦</td>
      </tr>
      <tr>
          <td>紀錄需要儲複雜資料（不只 boolean）</td>
          <td>是 — WeakMap value 可任意</td>
      </tr>
      <tr>
          <td>自家 application、debug 重要</td>
          <td>否 — <a href="../pattern-attribute-idempotency-marker/">attribute 標記</a> 在 inspector 可見</td>
      </tr>
      <tr>
          <td>紀錄要跨頁面持久化</td>
          <td>否 — 改用 storage / 後端</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：WeakMap idempotency 是 attribute 標記的「不污染 DOM 替代品」 — 在 library / framework 衝突情境必要、在自家 application 通常用 attribute 即可。GC 自動清理是 WeakMap 的特性、預設不用 Map / Set 是因為它們會 memory leak。</p>
]]></content:encoded></item><item><title>鍵盤可達性：focus indicator、tab 順序、escape 路徑</title><link>https://tarrragon.github.io/blog/report/keyboard-accessibility/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/keyboard-accessibility/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>鍵盤使用者導航三要素：focus 可見、tab 順序合理、有 escape 路徑。&lt;/strong> 三者任一缺失、鍵盤使用者就卡住。視覺使用者看不到 focus 也能用滑鼠繼續、鍵盤使用者沒有 fallback。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>鍵盤可達性&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>視覺呈現面的 a11y&lt;/strong>（對比 / 放大）由 &lt;a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>行動 / motor 使用者的 a11y&lt;/strong>（hit target）由 &lt;a href="../motor-accessibility-hit-target/">#53 Motor 可達性&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>DOM 移動時的 focus 處理&lt;/strong>由 &lt;a href="../focus-management-on-dom-move/">#37 focus management on DOM move&lt;/a> 處理（本篇處理「靜態 focus 設計」、#37 處理「動態 focus 移動」）&lt;/li>
&lt;/ul>&lt;/blockquote>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全盲（screen reader 使用者）&lt;/td>
 &lt;td>完全靠鍵盤、滑鼠看不到游標位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低視力&lt;/td>
 &lt;td>鍵盤比滑鼠精準（不需要瞄準）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Motor 障礙&lt;/td>
 &lt;td>鍵盤比滑鼠手部負擔小&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Power user&lt;/td>
 &lt;td>鍵盤比滑鼠快&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最後一類占人口比例不小 — 鍵盤可達性對全體使用者都有價值、不只 a11y 使用者。&lt;/p>
&lt;h3 id="三要素的失敗模式">三要素的失敗模式&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>要素&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;th>後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Focus 可見&lt;/td>
 &lt;td>&lt;code>outline: 0&lt;/code> 移除預設 focus 但沒補替代&lt;/td>
 &lt;td>鍵盤使用者不知道 focus 在哪、迷失&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab 順序&lt;/td>
 &lt;td>順序跟視覺布局不一致&lt;/td>
 &lt;td>跳來跳去、迷失&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Escape 路徑&lt;/td>
 &lt;td>Modal 沒有 ESC 關閉&lt;/td>
 &lt;td>卡在 modal 出不來&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三者都是「視覺使用者通常不會碰到、鍵盤使用者必碰」— 開發者用滑鼠測 100% OK、鍵盤使用者一進去就壞。&lt;/p>
&lt;hr>
&lt;h2 id="風險點-1focus-indicator-的可見度">風險點 1：Focus indicator 的可見度&lt;/h2>
&lt;p>&lt;strong>位置&lt;/strong>：tab focus 到 search input、scope radio、filter checkbox 等元素。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>瀏覽器預設 focus outline（藍色 2px）&lt;/li>
&lt;li>某些 theme 用 &lt;code>outline: 0&lt;/code> 移除 — 鍵盤使用者迷失&lt;/li>
&lt;li>自訂 outline 要對比足夠（WCAG 2.4.7、AA 3:1 對比 + 至少 2px 寬）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：鍵盤使用者 tab 過去看不到 focus 在哪、不知道下一個 enter 會激活誰。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：用 keyboard tab 過所有互動元素、確認每個都有可見 focus。&lt;/p>
&lt;p>&lt;strong>修正方向&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">/* 預設 — 信任瀏覽器 outline */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c">/* 不寫 outline: 0 */&lt;/span>
&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 class="c">/* 客製 — 用 :focus-visible（只在鍵盤觸發時顯示、滑鼠點擊不顯示） */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">focus-visible&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">outline&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="kc">solid&lt;/span> &lt;span class="kc">currentColor&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">outline-offset&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c">/* 移除 outline 必須補 box-shadow / border 等替代 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nt">button&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">focus&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">outline&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">box-shadow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="kt">px&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">focus&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="kc">color&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>&lt;code>:focus-visible&lt;/code> 是現代做法 — 滑鼠使用者不看到 outline（不會覺得「煩」）、鍵盤使用者看到 outline（必要的回饋）。&lt;/p>
&lt;h3 id="focus-indicator-的對比度">Focus indicator 的對比度&lt;/h3>
&lt;p>WCAG 2.4.11 要求 focus indicator 跟相鄰背景對比 ≥ 3:1：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>鍵盤使用者導航三要素：focus 可見、tab 順序合理、有 escape 路徑。</strong> 三者任一缺失、鍵盤使用者就卡住。視覺使用者看不到 focus 也能用滑鼠繼續、鍵盤使用者沒有 fallback。</p>
<blockquote>
<p>本篇焦點：<strong>鍵盤可達性</strong>。</p>
<ul>
<li><strong>視覺呈現面的 a11y</strong>（對比 / 放大）由 <a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助</a> 處理</li>
<li><strong>行動 / motor 使用者的 a11y</strong>（hit target）由 <a href="../motor-accessibility-hit-target/">#53 Motor 可達性</a> 處理</li>
<li><strong>DOM 移動時的 focus 處理</strong>由 <a href="../focus-management-on-dom-move/">#37 focus management on DOM move</a> 處理（本篇處理「靜態 focus 設計」、#37 處理「動態 focus 移動」）</li>
</ul></blockquote>
<hr>
<h2 id="為什麼鍵盤可達性需要獨立盤點">為什麼鍵盤可達性需要獨立盤點</h2>
<h3 id="使用者類型">使用者類型</h3>
<table>
  <thead>
      <tr>
          <th>使用者</th>
          <th>為什麼用鍵盤</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全盲（screen reader 使用者）</td>
          <td>完全靠鍵盤、滑鼠看不到游標位置</td>
      </tr>
      <tr>
          <td>低視力</td>
          <td>鍵盤比滑鼠精準（不需要瞄準）</td>
      </tr>
      <tr>
          <td>Motor 障礙</td>
          <td>鍵盤比滑鼠手部負擔小</td>
      </tr>
      <tr>
          <td>Power user</td>
          <td>鍵盤比滑鼠快</td>
      </tr>
  </tbody>
</table>
<p>最後一類占人口比例不小 — 鍵盤可達性對全體使用者都有價值、不只 a11y 使用者。</p>
<h3 id="三要素的失敗模式">三要素的失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>要素</th>
          <th>失敗模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Focus 可見</td>
          <td><code>outline: 0</code> 移除預設 focus 但沒補替代</td>
          <td>鍵盤使用者不知道 focus 在哪、迷失</td>
      </tr>
      <tr>
          <td>Tab 順序</td>
          <td>順序跟視覺布局不一致</td>
          <td>跳來跳去、迷失</td>
      </tr>
      <tr>
          <td>Escape 路徑</td>
          <td>Modal 沒有 ESC 關閉</td>
          <td>卡在 modal 出不來</td>
      </tr>
  </tbody>
</table>
<p>三者都是「視覺使用者通常不會碰到、鍵盤使用者必碰」— 開發者用滑鼠測 100% OK、鍵盤使用者一進去就壞。</p>
<hr>
<h2 id="風險點-1focus-indicator-的可見度">風險點 1：Focus indicator 的可見度</h2>
<p><strong>位置</strong>：tab focus 到 search input、scope radio、filter checkbox 等元素。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>瀏覽器預設 focus outline（藍色 2px）</li>
<li>某些 theme 用 <code>outline: 0</code> 移除 — 鍵盤使用者迷失</li>
<li>自訂 outline 要對比足夠（WCAG 2.4.7、AA 3:1 對比 + 至少 2px 寬）</li>
</ul>
<p><strong>症狀</strong>：鍵盤使用者 tab 過去看不到 focus 在哪、不知道下一個 enter 會激活誰。</p>
<p><strong>第一個該查的</strong>：用 keyboard tab 過所有互動元素、確認每個都有可見 focus。</p>
<p><strong>修正方向</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">/* 預設 — 信任瀏覽器 outline */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c">/* 不寫 outline: 0 */</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">/* 客製 — 用 :focus-visible（只在鍵盤觸發時顯示、滑鼠點擊不顯示） */</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">:</span><span class="nd">focus-visible</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">outline</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="kc">currentColor</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">outline-offset</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</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="c">/* 移除 outline 必須補 box-shadow / border 等替代 */</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nt">button</span><span class="p">:</span><span class="nd">focus</span> <span class="p">{</span> <span class="k">outline</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="k">box-shadow</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">0</span> <span class="mi">0</span> <span class="mi">3</span><span class="kt">px</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">focus</span><span class="o">-</span><span class="kc">color</span><span class="p">);</span> <span class="p">}</span></span></span></code></pre></div><p><code>:focus-visible</code> 是現代做法 — 滑鼠使用者不看到 outline（不會覺得「煩」）、鍵盤使用者看到 outline（必要的回饋）。</p>
<h3 id="focus-indicator-的對比度">Focus indicator 的對比度</h3>
<p>WCAG 2.4.11 要求 focus indicator 跟相鄰背景對比 ≥ 3:1：</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">/* 較差 — 灰底 + 灰 outline、對比不足 */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">button</span> <span class="p">{</span> <span class="k">background</span><span class="p">:</span> <span class="mh">#f0f0f0</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">button</span><span class="p">:</span><span class="nd">focus-visible</span> <span class="p">{</span> <span class="k">outline</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#cccccc</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c">/* 好 — 跟背景對比足夠 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">.</span><span class="nc">button</span><span class="p">:</span><span class="nd">focus-visible</span> <span class="p">{</span> <span class="k">outline</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#0066cc</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="風險點-2tab-順序與視覺布局的對齊">風險點 2：Tab 順序與視覺布局的對齊</h2>
<p><strong>位置</strong>：搜尋頁元素：H1 → search input → scope radio → results → filter sidebar。</p>
<p><strong>判讀</strong>：</p>
<p>預設 tab 順序 = DOM 順序。如果視覺布局跟 DOM 順序不一致（例如 sidebar 在右、但 DOM 在前）、鍵盤使用者體驗：</p>
<ul>
<li>Tab 1：H1（OK）</li>
<li>Tab 2：跑到 sidebar（視覺在右下、鍵盤跳過去）</li>
<li>Tab 3：search input（視覺在左上、鍵盤跳回來）</li>
</ul>
<p><strong>症狀</strong>：鍵盤使用者 tab 順序看似隨機、失去空間感。</p>
<p><strong>第一個該查的</strong>：用 keyboard tab 過所有互動元素、看 focus 移動順序是否符合視覺閱讀順序（左到右、上到下）。</p>
<p><strong>修正方向</strong>：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM 順序對齊視覺順序</td>
          <td>改 HTML 結構讓 DOM 順序就是 tab 順序</td>
      </tr>
      <tr>
          <td>用 <code>tabindex</code> 調整順序</td>
          <td>顯式控制 tab 順序（風險：違反 DOM 順序、對 screen reader 仍依 DOM）</td>
      </tr>
      <tr>
          <td>Skip link 跳過長 navigation</td>
          <td>讓鍵盤使用者快速跳到主內容</td>
      </tr>
  </tbody>
</table>
<p>預設選「DOM 順序對齊視覺順序」 — 不需要 <code>tabindex</code>、對所有 a11y 工具都正確。</p>
<h3 id="skip-link-設計">Skip link 設計</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">body</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;#main&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;skip-link&#34;</span><span class="p">&gt;</span>跳到主內容<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>...<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">main</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;main&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">main</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">body</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="p">.</span><span class="nc">skip-link</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</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"> 3</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">-40</span><span class="kt">px</span><span class="p">;</span>       <span class="c">/* 預設藏起來 */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">left</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">bg</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</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 class="nc">skip-link</span><span class="p">:</span><span class="nd">focus</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>            <span class="c">/* tab 到時顯示 */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>第一個 tab 焦點 = skip link、鍵盤使用者可以選擇跳過 nav 直達主內容。</p>
<hr>
<h2 id="風險點-3modal--overlay-的-escape-路徑">風險點 3：Modal / overlay 的 escape 路徑</h2>
<p><strong>位置</strong>：Pagefind drawer 在 mobile 模式展開、filter sidebar 在某些 layout 是 modal-like。</p>
<p><strong>判讀</strong>：</p>
<p>鍵盤使用者進入 modal 後需要：</p>
<ol>
<li>按 ESC 可以關閉</li>
<li>Tab 順序限制在 modal 內（focus trap、不會 tab 到背景元素）</li>
<li>關閉 modal 後 focus 回到觸發元素</li>
</ol>
<p>任一缺失 = 卡住。</p>
<p><strong>症狀</strong>：鍵盤使用者打開 filter drawer 後 tab 跑到背景元素、不知道怎麼關 drawer。</p>
<p><strong>第一個該查的</strong>：開啟 modal / drawer / overlay、按 ESC 看會不會關、tab 看會不會跑到背景。</p>
<p><strong>修正方向</strong>：</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="kd">function</span> <span class="nx">openModal</span><span class="p">(</span><span class="nx">modal</span><span class="p">,</span> <span class="nx">trigger</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">modal</span><span class="p">.</span><span class="nx">showModal</span><span class="o">?</span><span class="p">.()</span> <span class="o">||</span> <span class="p">(</span><span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s1">&#39;block&#39;</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="c1">// ESC 關閉
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="nx">modal</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;keydown&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span> <span class="o">===</span> <span class="s1">&#39;Escape&#39;</span><span class="p">)</span> <span class="nx">closeModal</span><span class="p">(</span><span class="nx">modal</span><span class="p">,</span> <span class="nx">trigger</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></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// Focus trap（簡化版）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">focusables</span> <span class="o">=</span> <span class="nx">modal</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;button, input, select, [tabindex]&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">focusables</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">?</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">modal</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;keydown&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span> <span class="o">!==</span> <span class="s1">&#39;Tab&#39;</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="kd">var</span> <span class="nx">first</span> <span class="o">=</span> <span class="nx">focusables</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="kd">var</span> <span class="nx">last</span> <span class="o">=</span> <span class="nx">focusables</span><span class="p">[</span><span class="nx">focusables</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">shiftKey</span> <span class="o">&amp;&amp;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span> <span class="o">===</span> <span class="nx">first</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span> <span class="nx">last</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">e</span><span class="p">.</span><span class="nx">shiftKey</span> <span class="o">&amp;&amp;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span> <span class="o">===</span> <span class="nx">last</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span> <span class="nx">first</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="kd">function</span> <span class="nx">closeModal</span><span class="p">(</span><span class="nx">modal</span><span class="p">,</span> <span class="nx">trigger</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="nx">modal</span><span class="p">.</span><span class="nx">close</span><span class="o">?</span><span class="p">.()</span> <span class="o">||</span> <span class="p">(</span><span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s1">&#39;none&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="nx">trigger</span><span class="o">?</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>  <span class="c1">// 焦點回觸發元素
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p><strong>用 <code>&lt;dialog&gt;</code> 元素自動 trap</strong>：</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="p">&lt;</span><span class="nt">dialog</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;filter-modal&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">dialog</span><span class="p">&gt;</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="nx">modal</span><span class="p">.</span><span class="nx">showModal</span><span class="p">();</span>  <span class="c1">// 自動 focus trap + ESC 處理
</span></span></span></code></pre></div><p><code>&lt;dialog&gt;</code> 是現代做法 — 鍵盤行為由瀏覽器處理、不需要手寫 trap 邏輯。</p>
<hr>
<h2 id="設計取捨focus-處理策略">設計取捨：focus 處理策略</h2>
<p>當需要客製 focus 視覺時、四種做法：</p>
<h3 id="a信任瀏覽器預設-outline這個專案的預設">A：信任瀏覽器預設 outline（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：完全不寫 <code>outline</code> 規則、瀏覽器藍色 outline 自動套用</li>
<li><strong>選 A 的理由</strong>：成本最低、跨瀏覽器一致、不會意外破壞</li>
<li><strong>適合</strong>：對 focus 視覺沒有強烈品牌需求</li>
<li><strong>代價</strong>：focus 看起來「不夠精緻」（瀏覽器預設不一定符合品牌風格）</li>
</ul>
<h3 id="b用-focus-visible-客製-outline">B：用 <code>:focus-visible</code> 客製 outline</h3>
<ul>
<li><strong>機制</strong>：<code>:focus-visible { outline: 2px solid var(--brand); }</code>、滑鼠點擊不顯示</li>
<li><strong>跟 A 的取捨</strong>：B 達到品牌一致性、滑鼠使用者不被「煩」；A 簡單但視覺一般</li>
<li><strong>B 比 A 好的情境</strong>：品牌設計嚴格要求 focus 視覺</li>
</ul>
<h3 id="c用-box-shadow-取代-outline">C：用 <code>box-shadow</code> 取代 outline</h3>
<ul>
<li><strong>機制</strong>：<code>:focus-visible { box-shadow: 0 0 0 3px var(--focus); outline: 0; }</code></li>
<li><strong>跟 B 的取捨</strong>：C 跟 outline 視覺差異是「跟著元素圓角」、適合圓角 UI；outline 永遠是矩形</li>
<li><strong>C 比 B 好的情境</strong>：圓角元素需要 focus 跟隨圓角</li>
</ul>
<h3 id="d完全移除-focus-indicator">D：完全移除 focus indicator</h3>
<ul>
<li><strong>機制</strong>：<code>*:focus { outline: 0; }</code>、不補替代</li>
<li><strong>成本特別高的原因</strong>：違反 WCAG 2.4.7、鍵盤使用者完全無法導航</li>
<li><strong>D 是反模式</strong>：違反 WCAG 2.4.7（合規層） — 即使品牌追求極簡、也該保留 focus indicator</li>
</ul>
<p>「邏輯 tab 順序」要素的詳細展開（DOM vs tabindex 的取捨、跟 mental model 對齊）見 <a href="../tab-order-mental-model-alignment/">#71 Tab Order = DOM Order = Mental Model 三者對齊</a>。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../focus-management-on-dom-move/">#37 Focus management on DOM move</a></td>
          <td>互補 — 本篇處理「靜態 focus 設計」、#37 處理「DOM 移動時 focus 該怎麼跟」</td>
      </tr>
      <tr>
          <td><a href="../native-html-over-aria-role/">#39 Native HTML 優先於 ARIA role</a></td>
          <td>用 <code>&lt;button&gt;</code> / <code>&lt;dialog&gt;</code> / <code>&lt;input&gt;</code> 等 native element、自動獲得正確 keyboard 行為</td>
      </tr>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a></td>
          <td>客製 focus 樣式時、注意不要打破 framework 內部的 focus 邏輯</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="開發階段檢查清單">開發階段檢查清單</h2>
<table>
  <thead>
      <tr>
          <th>檢查</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Focus 可見</td>
          <td>拔掉滑鼠、只用鍵盤、tab 過所有互動元素、確認每個都有可見 focus</td>
      </tr>
      <tr>
          <td>Focus 對比</td>
          <td>DevTools Contrast Ratio 量 focus indicator 跟背景對比 ≥ 3:1</td>
      </tr>
      <tr>
          <td>Tab 順序</td>
          <td>tab 過去確認順序符合視覺閱讀順序</td>
      </tr>
      <tr>
          <td>ESC 關閉</td>
          <td>開啟 modal / drawer、按 ESC 看會不會關</td>
      </tr>
      <tr>
          <td>Focus trap</td>
          <td>開啟 modal、tab 看是否限制在 modal 內</td>
      </tr>
      <tr>
          <td>Focus return</td>
          <td>關閉 modal、看 focus 是否回觸發元素</td>
      </tr>
  </tbody>
</table>
<p>每個 ~30 秒、開發完成前跑一輪。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>鍵盤使用者反映「不知道 focus 在哪」</td>
          <td>確認沒有 <code>outline: 0</code> 沒補替代、用 <code>:focus-visible</code></td>
      </tr>
      <tr>
          <td>Tab 順序看起來隨機</td>
          <td>DOM 順序對齊視覺順序、必要時用 skip link</td>
      </tr>
      <tr>
          <td>Modal 開啟後鍵盤使用者卡住</td>
          <td>加 ESC 關閉 + focus trap、或改用 <code>&lt;dialog&gt;</code></td>
      </tr>
      <tr>
          <td>Modal 關閉後 focus 跑到頁面開頭</td>
          <td>關閉時手動 <code>trigger.focus()</code></td>
      </tr>
      <tr>
          <td>Focus 在 dark mode 看不清</td>
          <td>加對比度檢查（≥ 3:1）</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：鍵盤可達性的三要素都是「視覺使用者通常不會碰、鍵盤使用者必碰」 — 開發階段必須拔滑鼠測一輪、不能依賴使用者通報。</p>
]]></content:encoded></item><item><title>Motor 可達性：hit target、間距、誤點防護</title><link>https://tarrragon.github.io/blog/report/motor-accessibility-hit-target/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/motor-accessibility-hit-target/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Hit target ≥ 44×44px、相鄰互動元素之間有間距、避免「精準瞄準」需求。&lt;/strong> Motor accessibility 處理的不是視覺、是「手能否準確點擊」 — 行動裝置使用者、年長使用者、motor 障礙使用者都受益。設計時優先擴大 padding、不是縮小視覺。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>motor 可達性&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>視覺呈現面的 a11y&lt;/strong>由 &lt;a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>鍵盤使用者的 a11y&lt;/strong>由 &lt;a href="../keyboard-accessibility/">#52 鍵盤可達性&lt;/a> 處理&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼-motor-可達性需要獨立盤點">為什麼 motor 可達性需要獨立盤點&lt;/h2>
&lt;h3 id="使用者類型">使用者類型&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>使用者&lt;/th>
 &lt;th>為什麼 hit target 重要&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>行動裝置使用者&lt;/td>
 &lt;td>手指比滑鼠粗、需要更大目標&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>年長使用者&lt;/td>
 &lt;td>手部精準度下降&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Motor 障礙使用者&lt;/td>
 &lt;td>Tremor / 手部協調困難&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>暫時受限使用者（拿東西單手操作、晃動環境）&lt;/td>
 &lt;td>短期內精準度下降&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最後一類包含「正常使用者在某些情境」 — motor a11y 的設計對全體使用者都有價值。&lt;/p>
&lt;h3 id="失敗模式">失敗模式&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>影響範圍&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Hit target &amp;lt; 24px&lt;/td>
 &lt;td>行動裝置上難點&lt;/td>
 &lt;td>多數行動使用者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>相鄰互動元素間距不足&lt;/td>
 &lt;td>誤觸隔壁&lt;/td>
 &lt;td>手指粗 / motor 障礙者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要精準 drag / pinch&lt;/td>
 &lt;td>部分 motor 障礙者無法&lt;/td>
 &lt;td>motor 障礙者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>短時間內需多次精準操作&lt;/td>
 &lt;td>tremor 使用者無法&lt;/td>
 &lt;td>tremor 使用者&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="wcag-標準">WCAG 標準&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>2.5.5 Target Size&lt;/td>
 &lt;td>互動元素 ≥ 44×44 CSS px&lt;/td>
 &lt;td>AAA&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2.5.8 Target Size (Minimum)&lt;/td>
 &lt;td>互動元素 ≥ 24×24 CSS px（除非有間距足夠的等價替代）&lt;/td>
 &lt;td>AA（WCAG 2.2 新增）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2.5.7 Dragging Movements&lt;/td>
 &lt;td>拖拽動作有單擊替代&lt;/td>
 &lt;td>AA（WCAG 2.2 新增）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>WCAG 2.2 把 motor a11y 從 AAA 拉到部分 AA — 顯示這類問題的重要性提升。&lt;/p>
&lt;hr>
&lt;h2 id="風險點-1hit-target-太小">風險點 1：Hit target 太小&lt;/h2>
&lt;p>&lt;strong>位置&lt;/strong>：scope UI 的 radio buttons、filter checkbox。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>WCAG 2.5.5（AAA）建議互動元素 hit target ≥ 44×44px&lt;/li>
&lt;li>Native &lt;code>&amp;lt;input type=&amp;quot;radio&amp;quot;&amp;gt;&lt;/code> 在桌面 ~13×13px、行動裝置 24×24px&lt;/li>
&lt;li>label 包住 input + 文字、整個 label 可點 — 提升 hit target&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：行動裝置使用者點擊精準度不足、誤點旁邊選項。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：量 label 整體（含 padding）的高度與寬度。&lt;/p>
&lt;p>&lt;strong>修正方向&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">/* 較差 — input 視覺很小、label 文字緊鄰 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="nt">label&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">inline-block&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">input&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;radio&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">width&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">13&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">height&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">13&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c">/* 好 — label 整個區域可點、padding 撐到 44px */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nt">label&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">display&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">inline-flex&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">align-items&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">center&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">padding&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.625&lt;/span>&lt;span class="kt">rem&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="kt">rem&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c">/* 約 44px 高 */&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">cursor&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">pointer&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="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nt">input&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;radio&amp;#34;&lt;/span>&lt;span class="o">]&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">margin-right&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.5&lt;/span>&lt;span class="kt">rem&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>關鍵不是把 input 視覺變大、是把可點區域擴大（padding）— 視覺保持精緻、可點區域達標。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Hit target ≥ 44×44px、相鄰互動元素之間有間距、避免「精準瞄準」需求。</strong> Motor accessibility 處理的不是視覺、是「手能否準確點擊」 — 行動裝置使用者、年長使用者、motor 障礙使用者都受益。設計時優先擴大 padding、不是縮小視覺。</p>
<blockquote>
<p>本篇焦點：<strong>motor 可達性</strong>。</p>
<ul>
<li><strong>視覺呈現面的 a11y</strong>由 <a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助</a> 處理</li>
<li><strong>鍵盤使用者的 a11y</strong>由 <a href="../keyboard-accessibility/">#52 鍵盤可達性</a> 處理</li>
</ul></blockquote>
<hr>
<h2 id="為什麼-motor-可達性需要獨立盤點">為什麼 motor 可達性需要獨立盤點</h2>
<h3 id="使用者類型">使用者類型</h3>
<table>
  <thead>
      <tr>
          <th>使用者</th>
          <th>為什麼 hit target 重要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>行動裝置使用者</td>
          <td>手指比滑鼠粗、需要更大目標</td>
      </tr>
      <tr>
          <td>年長使用者</td>
          <td>手部精準度下降</td>
      </tr>
      <tr>
          <td>Motor 障礙使用者</td>
          <td>Tremor / 手部協調困難</td>
      </tr>
      <tr>
          <td>暫時受限使用者（拿東西單手操作、晃動環境）</td>
          <td>短期內精準度下降</td>
      </tr>
  </tbody>
</table>
<p>最後一類包含「正常使用者在某些情境」 — motor a11y 的設計對全體使用者都有價值。</p>
<h3 id="失敗模式">失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>失敗</th>
          <th>表現</th>
          <th>影響範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hit target &lt; 24px</td>
          <td>行動裝置上難點</td>
          <td>多數行動使用者</td>
      </tr>
      <tr>
          <td>相鄰互動元素間距不足</td>
          <td>誤觸隔壁</td>
          <td>手指粗 / motor 障礙者</td>
      </tr>
      <tr>
          <td>需要精準 drag / pinch</td>
          <td>部分 motor 障礙者無法</td>
          <td>motor 障礙者</td>
      </tr>
      <tr>
          <td>短時間內需多次精準操作</td>
          <td>tremor 使用者無法</td>
          <td>tremor 使用者</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="wcag-標準">WCAG 標準</h2>
<table>
  <thead>
      <tr>
          <th>標準</th>
          <th>要求</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>2.5.5 Target Size</td>
          <td>互動元素 ≥ 44×44 CSS px</td>
          <td>AAA</td>
      </tr>
      <tr>
          <td>2.5.8 Target Size (Minimum)</td>
          <td>互動元素 ≥ 24×24 CSS px（除非有間距足夠的等價替代）</td>
          <td>AA（WCAG 2.2 新增）</td>
      </tr>
      <tr>
          <td>2.5.7 Dragging Movements</td>
          <td>拖拽動作有單擊替代</td>
          <td>AA（WCAG 2.2 新增）</td>
      </tr>
  </tbody>
</table>
<p>WCAG 2.2 把 motor a11y 從 AAA 拉到部分 AA — 顯示這類問題的重要性提升。</p>
<hr>
<h2 id="風險點-1hit-target-太小">風險點 1：Hit target 太小</h2>
<p><strong>位置</strong>：scope UI 的 radio buttons、filter checkbox。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>WCAG 2.5.5（AAA）建議互動元素 hit target ≥ 44×44px</li>
<li>Native <code>&lt;input type=&quot;radio&quot;&gt;</code> 在桌面 ~13×13px、行動裝置 24×24px</li>
<li>label 包住 input + 文字、整個 label 可點 — 提升 hit target</li>
</ul>
<p><strong>症狀</strong>：行動裝置使用者點擊精準度不足、誤點旁邊選項。</p>
<p><strong>第一個該查的</strong>：量 label 整體（含 padding）的高度與寬度。</p>
<p><strong>修正方向</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">/* 較差 — input 視覺很小、label 文字緊鄰 */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">label</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">inline-block</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">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s2">&#34;radio&#34;</span><span class="o">]</span> <span class="p">{</span> <span class="k">width</span><span class="p">:</span> <span class="mi">13</span><span class="kt">px</span><span class="p">;</span> <span class="k">height</span><span class="p">:</span> <span class="mi">13</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></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c">/* 好 — label 整個區域可點、padding 撐到 44px */</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">label</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">inline-flex</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="k">align-items</span><span class="p">:</span> <span class="kc">center</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mf">0.625</span><span class="kt">rem</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>  <span class="c">/* 約 44px 高 */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">cursor</span><span class="p">:</span> <span class="kc">pointer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s2">&#34;radio&#34;</span><span class="o">]</span> <span class="p">{</span> <span class="k">margin-right</span><span class="p">:</span> <span class="mf">0.5</span><span class="kt">rem</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>關鍵不是把 input 視覺變大、是把可點區域擴大（padding）— 視覺保持精緻、可點區域達標。</p>
<hr>
<h2 id="風險點-2相鄰互動元素間距不足">風險點 2：相鄰互動元素間距不足</h2>
<p><strong>位置</strong>：filter checkbox 列、scope radio 列。</p>
<p><strong>判讀</strong>：</p>
<p>兩個 hit target 緊鄰、即使各自達 44px、相鄰時仍可能誤觸 — WCAG 2.5.8 要求「目標之間有足夠間距」。</p>
<p><strong>症狀</strong>：使用者想點 A 但點到旁邊的 B。</p>
<p><strong>修正方向</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">/* 加 gap 確保相鄰元素間距 */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="p">.</span><span class="nc">filter-list</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="kc">flex</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">flex-direction</span><span class="p">:</span> <span class="kc">column</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">gap</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>  <span class="c">/* 至少 8px 間距 */</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">/* 或用 padding 撐開 */</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">.</span><span class="nc">filter-list</span> <span class="nt">label</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mf">0.625</span><span class="kt">rem</span> <span class="mi">1</span><span class="kt">rem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">margin-bottom</span><span class="p">:</span> <span class="mi">4</span><span class="kt">px</span><span class="p">;</span>  <span class="c">/* 加總間距達 8px+ */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>預設 8px 間距 — 比視覺需求多一點、避免誤觸。</p>
<hr>
<h2 id="風險點-3需要精準-drag--pinch-的操作">風險點 3：需要精準 drag / pinch 的操作</h2>
<p><strong>位置</strong>：搜尋頁未實作 drag 互動、但若未來加（例如拖拽結果排序、pinch 縮放圖片）。</p>
<p><strong>判讀</strong>：</p>
<p>WCAG 2.5.7（AA）要求 drag 動作有單擊替代 — 例如「拖拽排序」要有「上移 / 下移」按鈕作為替代。</p>
<p><strong>症狀</strong>：motor 障礙使用者無法完成 drag 操作。</p>
<p><strong>修正方向</strong>：</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;!-- 主互動：drag --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">li</span> <span class="na">draggable</span><span class="o">=</span><span class="s">&#34;true&#34;</span><span class="p">&gt;</span>項目 A<span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</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">&lt;!-- 必須提供：button 替代 --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  項目 A
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;上移&#34;</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">8</span><span class="cl">  <span class="p">&lt;</span><span class="nt">button</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;下移&#34;</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">9</span><span class="cl"><span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span></span></span></code></pre></div><p>對搜尋頁當前實作不適用、但未來加互動時的預警。</p>
<hr>
<h2 id="設計取捨擴大-hit-target-的策略">設計取捨：擴大 hit target 的策略</h2>
<p>當「視覺精緻度」與「hit target 大小」衝突、四種做法：</p>
<h3 id="a視覺保持小padding-擴大可點區這個專案的預設">A：視覺保持小、padding 擴大可點區（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：input 視覺 13px、label padding 撐到 44px</li>
<li><strong>選 A 的理由</strong>：視覺精緻 + a11y 達標、兩全</li>
<li><strong>適合</strong>：絕大多數互動元素</li>
<li><strong>代價</strong>：UI 整體高度增加（每行 44px+）</li>
</ul>
<h3 id="b視覺直接放大到-44px">B：視覺直接放大到 44px</h3>
<ul>
<li><strong>機制</strong>：input width: 44px; height: 44px;</li>
<li><strong>跟 A 的取捨</strong>：B 視覺粗、A 視覺精緻；B 在「需要清楚看到」的情境（年長使用者）有價值</li>
<li><strong>B 比 A 好的情境</strong>：使用者主要是年長者、視覺辨識比精緻重要</li>
</ul>
<h3 id="c視覺小不擴-padding不滿足-a11y">C：視覺小、不擴 padding（不滿足 a11y）</h3>
<ul>
<li><strong>機制</strong>：input 13px、label 緊鄰文字、無 padding</li>
<li><strong>成本特別高的原因</strong>：行動使用者誤點、motor 障礙者無法用、違反 WCAG 2.5.8</li>
<li><strong>C 才合理的情境</strong>：純 desktop 應用 + 確認使用者群不含行動 / motor — 通常不該假設</li>
</ul>
<h3 id="d用-hover-area-擴大命中hover-才放大">D：用 hover area 擴大命中（hover 才放大）</h3>
<ul>
<li><strong>機制</strong>：預設視覺小、hover 時擴大可點區</li>
<li><strong>跟 A 的取捨</strong>：D 在 desktop 視覺精緻、hover 反饋也好；行動裝置沒有 hover、D 失敗</li>
<li><strong>D 比 A 好的情境</strong>：純 desktop 工具</li>
</ul>
<hr>
<h2 id="設計取捨誤點防護機制">設計取捨：誤點防護機制</h2>
<p>對「誤點代價高」的操作（刪除 / 提交 / 付款）、四種做法：</p>
<h3 id="a直接觸發--後續-undo這個專案的預設若有此類操作">A：直接觸發 + 後續 undo（這個專案的預設、若有此類操作）</h3>
<ul>
<li><strong>機制</strong>：點擊立刻執行、提供 undo 機制（例如 toast「已刪除、5 秒內可復原」）</li>
<li><strong>選 A 的理由</strong>：常見操作流暢、誤點有救</li>
<li><strong>適合</strong>：可逆操作（刪除、移動、隱藏）</li>
<li><strong>代價</strong>：實作 undo 機制需要儲狀態</li>
</ul>
<h3 id="b點擊--確認對話框">B：點擊 → 確認對話框</h3>
<ul>
<li><strong>機制</strong>：點擊出 confirm dialog「確定要 X 嗎？」</li>
<li><strong>跟 A 的取捨</strong>：B 防誤點更強、A 流程更順；B 的成本是「正常使用者也要多一步」</li>
<li><strong>B 比 A 好的情境</strong>：不可逆操作（永久刪除、付款）</li>
</ul>
<h3 id="c長按觸發">C：長按觸發</h3>
<ul>
<li><strong>機制</strong>：需要長按 1 秒才觸發、誤點不會</li>
<li><strong>跟 A/B 的取捨</strong>：C 對 motor 障礙不友善（需要持續按）、且不直觀</li>
<li><strong>C 是反模式</strong>：對 motor 障礙不友善（需要持續按 1 秒）— 不直觀、違反 a11y 預期互動</li>
</ul>
<h3 id="d拖到確認區">D：拖到「確認區」</h3>
<ul>
<li><strong>機制</strong>：滑動到特定區域才觸發（iOS 拖刪除）</li>
<li><strong>跟 A/B 的取捨</strong>：D 對非典型互動使用者不直觀、違反 WCAG 2.5.7（需 button 替代）</li>
<li><strong>D 才合理的情境</strong>：搭配 button 替代（drag + button 兩種途徑都行）</li>
</ul>
<hr>
<h2 id="開發階段檢查清單">開發階段檢查清單</h2>
<table>
  <thead>
      <tr>
          <th>檢查</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hit target ≥ 44px</td>
          <td>DevTools Box Model 量 interactive 元素的 padding box</td>
      </tr>
      <tr>
          <td>相鄰元素間距 ≥ 8px</td>
          <td>DevTools 看 gap / margin</td>
      </tr>
      <tr>
          <td>行動裝置實測</td>
          <td>DevTools Device Mode + 實機測試</td>
      </tr>
      <tr>
          <td>不可逆操作有確認</td>
          <td>點擊「刪除」看是否有 confirm</td>
      </tr>
      <tr>
          <td>Drag 操作有 button 替代</td>
          <td>任何 drag 互動都有對應 button</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助</a></td>
          <td>互補 — 視覺面 vs 操作面、不同使用者群</td>
      </tr>
      <tr>
          <td><a href="../keyboard-accessibility/">#52 鍵盤可達性</a></td>
          <td>互補 — 鍵盤是 motor a11y 的一個面向（鍵盤精準度 &gt; 滑鼠 &gt; 觸控）、本篇處理觸控 / 點擊面</td>
      </tr>
      <tr>
          <td><a href="../native-html-over-aria-role/">#39 Native HTML 優先於 ARIA role</a></td>
          <td>用 native button / input 自動獲得合理 hit area、不需自行設計</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>行動使用者反映誤點</td>
          <td>量 hit target、&lt; 44px 加 padding</td>
      </tr>
      <tr>
          <td>「我這個介面只給 desktop 用」</td>
          <td>行動使用者比例可能比想像高、量化驗證</td>
      </tr>
      <tr>
          <td>Drag 互動沒有 button 替代</td>
          <td>加 button、達 WCAG 2.5.7</td>
      </tr>
      <tr>
          <td>不可逆操作沒有 confirm</td>
          <td>加 confirm dialog</td>
      </tr>
      <tr>
          <td>Filter list 元素緊鄰、容易誤觸</td>
          <td>加 gap ≥ 8px</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Motor a11y 是「手能否準確點擊」 — 不只給 motor 障礙使用者、行動使用者 / 年長者 / 暫時受限使用者都受益。預設 padding 擴 44px、間距 8px、不可逆操作加 confirm — 這些是基礎、不是優化。</p>
]]></content:encoded></item><item><title>Pattern：跨 slot 同節點搬遷</title><link>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">place&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matches&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="nx">desktopSlot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&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="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始化
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。&lt;/p>
&lt;p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。&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>Filter UI 跨 viewport 切換顯示位置&lt;/td>
 &lt;td>checkbox state 跟著節點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Modal 內容 vs 側邊抽屜&lt;/td>
 &lt;td>同一份表單在兩種展示方式間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab UI 跨 desktop / mobile 重新組織&lt;/td>
 &lt;td>各 tab 內 state 不重置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任何「同 UI、不同位置」的 responsive 切換&lt;/td>
 &lt;td>不需要 state 同步邏輯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。&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;/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>UI 純 stateless（純圖示、純文字）&lt;/td>
 &lt;td>複製兩份成本低、無 state 風險&lt;/td>
 &lt;td>CSS-only 雙顯示 + display 切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 管的節點&lt;/td>
 &lt;td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）&lt;/td>
 &lt;td>必須搬整節點、不複製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩個位置視覺差異大&lt;/td>
 &lt;td>搬遷後 UI 不適配新位置&lt;/td>
 &lt;td>各自獨立元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="appendchild-是搬遷不是複製">&lt;code>appendChild&lt;/code> 是搬遷、不是複製&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">parentA&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// node 從原位置消失、出現在 parentA
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM API 的 &lt;code>appendChild&lt;/code> / &lt;code>insertBefore&lt;/code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。&lt;/p>
&lt;h3 id="初始放在哪">初始放在哪&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 預設位置（mobile / fallback）--&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pagefind-ui&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;drawer&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-panel&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- 初始在這 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;!-- 桌面 slot（空、等待搬入）--&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">aside&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;desktop-filter-slot&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">aside&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。&lt;/p>
&lt;h3 id="跨-slot-切換的時機">跨 slot 切換的時機&lt;/h3>
&lt;p>&lt;code>matchMedia&lt;/code> event 是 viewport 跨過 breakpoint 的瞬間：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始也跑一次
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。&lt;code>matchMedia&lt;/code> 只在 cross 的瞬間觸發。&lt;/p>
&lt;h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity&lt;/h3>
&lt;p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<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">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</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">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</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 class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始化
</span></span></span></code></pre></div><p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。</p>
<p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter UI 跨 viewport 切換顯示位置</td>
          <td>checkbox state 跟著節點</td>
      </tr>
      <tr>
          <td>Modal 內容 vs 側邊抽屜</td>
          <td>同一份表單在兩種展示方式間</td>
      </tr>
      <tr>
          <td>Tab UI 跨 desktop / mobile 重新組織</td>
          <td>各 tab 內 state 不重置</td>
      </tr>
      <tr>
          <td>任何「同 UI、不同位置」的 responsive 切換</td>
          <td>不需要 state 同步邏輯</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。</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>UI 純 stateless（純圖示、純文字）</td>
          <td>複製兩份成本低、無 state 風險</td>
          <td>CSS-only 雙顯示 + display 切換</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）</td>
          <td>必須搬整節點、不複製</td>
      </tr>
      <tr>
          <td>兩個位置視覺差異大</td>
          <td>搬遷後 UI 不適配新位置</td>
          <td>各自獨立元件</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="appendchild-是搬遷不是複製"><code>appendChild</code> 是搬遷、不是複製</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="nx">parentA</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">node</span><span class="p">);</span>  <span class="c1">// node 從原位置消失、出現在 parentA
</span></span></span></code></pre></div><p>DOM API 的 <code>appendChild</code> / <code>insertBefore</code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。</p>
<h3 id="初始放在哪">初始放在哪</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;!-- 預設位置（mobile / fallback）--&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;pagefind-ui&#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">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;drawer&#34;</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">class</span><span class="o">=</span><span class="s">&#34;filter-panel&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>  <span class="c">&lt;!-- 初始在這 --&gt;</span>
</span></span><span class="line"><span class="ln">5</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">6</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">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c">&lt;!-- 桌面 slot（空、等待搬入）--&gt;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">&lt;</span><span class="nt">aside</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;desktop-filter-slot&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">aside</span><span class="p">&gt;</span></span></span></code></pre></div><p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。</p>
<h3 id="跨-slot-切換的時機">跨 slot 切換的時機</h3>
<p><code>matchMedia</code> event 是 viewport 跨過 breakpoint 的瞬間：</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="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始也跑一次
</span></span></span></code></pre></div><p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。<code>matchMedia</code> 只在 cross 的瞬間觸發。</p>
<h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity</h3>
<p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。</p>
<p>詳細安全規則由 <a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a> 處理。</p>
<h3 id="focus-跟著搬">Focus 跟著搬</h3>
<p>搬遷可能讓鍵盤 focus 暫時失去（視瀏覽器）— 加 save/restore：</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="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</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">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</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">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</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></code></pre></div><p>詳細處理由 <a href="../focus-management-on-dom-move/">#37 動態 DOM 移動時的 focus 管理</a> 處理。</p>
<hr>
<h2 id="設計取捨兩個-slot-的-stateful-ui-共用">設計取捨：兩個 slot 的 stateful UI 共用</h2>
<p>四種做法、各自機會成本不同。預設選 A（搬同節點）、其他做法在特定情境合理。</p>
<h3 id="a搬同一節點這個專案的預設">A：搬同一節點（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>matchMedia + appendChild</code> 在兩 slot 間搬同一份節點</li>
<li><strong>選 A 的理由</strong>：state 跟著節點、切換無感、不需要 sync 邏輯</li>
<li><strong>適合</strong>：stateful UI、需要在兩個位置展示同樣內容</li>
<li><strong>代價</strong>：搬遷 callback 在 viewport 跨 breakpoint 時觸發、需要處理 focus / 動畫</li>
<li><strong>詳細</strong>：本卡片</li>
</ul>
<h3 id="bcss-only-雙顯示--display-切換">B：CSS-only 雙顯示 + display 切換</h3>
<ul>
<li><strong>機制</strong>：兩個位置都放同一份節點 (寫兩遍 HTML)、用 <code>@media + display: none</code> 切換顯示</li>
<li><strong>跟 A 的取捨</strong>：B 純 CSS 簡單、A 需要 JS；但 B 對 stateful UI 失敗（兩份 state 各自獨立）</li>
<li><strong>B 比 A 好的情境</strong>：UI 純 stateless（純圖示）、純 CSS 解就夠</li>
</ul>
<h3 id="ccss-only--js-同步-state">C：CSS-only + JS 同步 state</h3>
<ul>
<li><strong>機制</strong>：兩份節點 + JS 監聽 state 變動同步</li>
<li><strong>跟 A 的取捨</strong>：C 比 B 解 state 問題、但同步邏輯複雜（雙向更新、避免循環）</li>
<li><strong>C 比 A 好的情境</strong>：兩個位置的 UI 視覺需要差異（不只是位置不同）</li>
</ul>
<h3 id="djs-完全重建-ui">D：JS 完全重建 UI</h3>
<ul>
<li><strong>機制</strong>：viewport 變動時拆掉舊 UI、在新位置重建一份</li>
<li><strong>成本特別高的原因</strong>：state 在重建時遺失、UI 閃爍、輸入中斷</li>
<li><strong>D 才合理的情境</strong>：UI 是 stateless 的、且重建成本低</li>
</ul>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 pattern 的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法、本卡片是「跨 slot 搬遷」這個專門情境的補充：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應 pattern</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 的起點</td>
          <td><a href="../pattern-document-query/">#46 document</a> / <a href="../pattern-component-root/">#47 元件根變數</a> / <a href="../pattern-root-as-parameter/">#48 起點當參數</a> / <a href="../pattern-closest-lookup/">#49 closest 反向</a></td>
      </tr>
      <tr>
          <td>Idempotency 過濾</td>
          <td><a href="../pattern-attribute-idempotency-marker/">#50 attribute 標記</a> / <a href="../pattern-weakmap-idempotency-record/">#51 WeakMap</a></td>
      </tr>
      <tr>
          <td>跨 slot 搬遷（本卡片）</td>
          <td>同節點 vs 雙節點 + state 同步</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="應用範例跨-viewport-filter-切換">應用範例：跨 viewport filter 切換</h2>





<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">setupResponsiveFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">breakpoint</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">filter</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</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="kd">var</span> <span class="nx">desktopSlot</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-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">filter</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span> <span class="o">||</span> <span class="o">!</span><span class="nx">desktopSlot</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: &#39;</span> <span class="o">+</span> <span class="nx">breakpoint</span> <span class="o">+</span> <span class="s1">&#39;px)&#39;</span><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="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>完整 pattern：取元件根 + matchMedia + 搬遷 + focus 處理。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩份節點各自 state、用 sync 邏輯保持一致</td>
          <td>是 — 改成搬同節點、移除 sync</td>
      </tr>
      <tr>
          <td>Stateful UI 在 mobile / desktop 兩種 layout 間</td>
          <td>是 — 直接的應用</td>
      </tr>
      <tr>
          <td>切換 viewport 時 UI 閃爍 / 重建</td>
          <td>是 — 改成搬而非重建</td>
      </tr>
      <tr>
          <td>兩個位置展示完全不同的 UI（不是同邏輯）</td>
          <td>否 — 各自獨立元件</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>是 — 整節點搬安全、但要遵守 <a href="../component-boundary-and-js-impact/">#13</a> 的規則</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Stateful UI 的兩個展示位置共用同一份節點、state 自然跟著走 — 比「兩份節點 + sync 邏輯」乾淨。複製兩份是「state 來源從一變二」的隱形多源（違反 <a href="../single-source-of-truth/">#44 SSoT</a>）。</p>
]]></content:encoded></item><item><title>Filter 與 Source 的抽象層錯位</title><link>https://tarrragon.github.io/blog/report/view-layer-filter-vs-source-layer/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/view-layer-filter-vs-source-layer/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Filter 必須跟它過濾的資料源在同一層運作。&lt;/strong> 把 filter 寫在視覺層（querySelector + show/hide）、把 source 留在資料層分批產出（paginated fetch / streaming / lazy iterator）— 兩層的「一筆」定義不一致、filter 看不到 source 還沒產出的東西、結果跟使用者意圖之間有語意縫。&lt;/p>
&lt;p>更廣義的說法：&lt;strong>stream 操作（filter / sort / count / transform / search）必須跟 stream 的 materialization 同層或更上游&lt;/strong>。在下游做 stream 操作、操作的對象是已經 materialize 的 subset、不是完整的 stream。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>資料層&lt;/td>
 &lt;td>Source 產出的一筆 record&lt;/td>
 &lt;td>全部、或還沒產出的下一批&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>渲染層&lt;/td>
 &lt;td>已 render 進 DOM 的一筆&lt;/td>
 &lt;td>= 已 fetch 並 render 過的子集&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>視覺層&lt;/td>
 &lt;td>螢幕上看得見的一筆&lt;/td>
 &lt;td>= render 層之中沒被 hide 的子集&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Filter 寫在視覺層、它的「過濾全部」≡「過濾螢幕上看得見的全部」≡「過濾已 fetch 已 render 的子集」。&lt;strong>離資料層的真實全集差兩層&lt;/strong>。使用者意圖（「給我所有 title 含 X 的結果」）對應的是資料層的全集、不是視覺層的子集。&lt;/p>
&lt;h3 id="silent-失敗的條件">Silent 失敗的條件&lt;/h3>
&lt;p>層錯位不會在「filter 子集裡有命中」的情境下被發現。它只在以下條件下顯露：&lt;/p>
&lt;ol>
&lt;li>已 materialize 的子集裡剛好沒命中&lt;/li>
&lt;li>但完整 stream 裡有命中、只是還沒 materialize&lt;/li>
&lt;li>使用者沒有訊號知道「還有沒抓的」&lt;/li>
&lt;/ol>
&lt;p>三個條件同時滿足、使用者看到「filter 後是空的」、誤以為是「沒有命中」、放棄。&lt;/p>
&lt;h3 id="為什麼這個-bug-容易寫出來">為什麼這個 bug 容易寫出來&lt;/h3>
&lt;p>視覺層 filter 是寫起來最簡單的版本：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">display&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">dataset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">title&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">includes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">?&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span> &lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;none&amp;#39;&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="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>5 行解決、看起來能用、第一輪測試（手動輸入 query → 看到 filter 生效）會通過。&lt;strong>「能用」的訊號出現太早、掩蓋了語意缺口&lt;/strong>。&lt;/p>
&lt;p>這是 &lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a> 在「filter × source」情境的具體展現 — 容易寫的位置（已 materialize 的 view 層）跟對齊意圖的位置（source 層）方向相反。&lt;/p>
&lt;hr>
&lt;h2 id="哪些-source-形狀有層錯位風險">哪些 source 形狀有層錯位風險&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Source 型態&lt;/th>
 &lt;th>是否有層錯位風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一次性 fetch、靜態陣列&lt;/td>
 &lt;td>否（沒有 subset）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paginated fetch（load more / cursor）&lt;/td>
 &lt;td>是 — 本次任務的 case&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Streaming（SSE / WebSocket）&lt;/td>
 &lt;td>視 server 是否限額&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lazy iterator + take(N) / break&lt;/td>
 &lt;td>是&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cached + revalidate&lt;/td>
 &lt;td>是（cache vs fresh 兩 dataset）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四類 source 共用同個結構：&lt;strong>source 分批 / 限額 / 延遲 materialize、filter 在下游 → silent 缺口&lt;/strong>。詳細形狀分析見 &lt;a href="../data-source-shape-defines-feature-shape/">#63 資料源的形狀決定 feature 的形狀&lt;/a>。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的實際情境">這次任務的實際情境&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>搜尋頁實作 title / content filter：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// pagefind 分批 load (load more 按鈕)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">pagefind&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nx">results&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">results&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">slice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">start&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">start&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">container&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 我們在 view 層 post-filter
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">applyFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scope&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"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.result&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">hidden&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">matchesScope&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scope&lt;/span>&lt;span class="p">);&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跑出來的問題：使用者選 title-only filter、第二批 8 筆全部 title 不含 query → 點 &amp;ldquo;load more&amp;rdquo; 後畫面閃了一下、新增的 8 筆全 hidden、使用者看到的內容沒變。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Filter 必須跟它過濾的資料源在同一層運作。</strong> 把 filter 寫在視覺層（querySelector + show/hide）、把 source 留在資料層分批產出（paginated fetch / streaming / lazy iterator）— 兩層的「一筆」定義不一致、filter 看不到 source 還沒產出的東西、結果跟使用者意圖之間有語意縫。</p>
<p>更廣義的說法：<strong>stream 操作（filter / sort / count / transform / search）必須跟 stream 的 materialization 同層或更上游</strong>。在下游做 stream 操作、操作的對象是已經 materialize 的 subset、不是完整的 stream。</p>
<hr>
<h2 id="為什麼層錯位產生語意縫">為什麼層錯位產生語意縫</h2>
<h3 id="一筆在不同層有不同定義">「一筆」在不同層有不同定義</h3>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>「一筆」是什麼</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料層</td>
          <td>Source 產出的一筆 record</td>
          <td>全部、或還沒產出的下一批</td>
      </tr>
      <tr>
          <td>渲染層</td>
          <td>已 render 進 DOM 的一筆</td>
          <td>= 已 fetch 並 render 過的子集</td>
      </tr>
      <tr>
          <td>視覺層</td>
          <td>螢幕上看得見的一筆</td>
          <td>= render 層之中沒被 hide 的子集</td>
      </tr>
  </tbody>
</table>
<p>Filter 寫在視覺層、它的「過濾全部」≡「過濾螢幕上看得見的全部」≡「過濾已 fetch 已 render 的子集」。<strong>離資料層的真實全集差兩層</strong>。使用者意圖（「給我所有 title 含 X 的結果」）對應的是資料層的全集、不是視覺層的子集。</p>
<h3 id="silent-失敗的條件">Silent 失敗的條件</h3>
<p>層錯位不會在「filter 子集裡有命中」的情境下被發現。它只在以下條件下顯露：</p>
<ol>
<li>已 materialize 的子集裡剛好沒命中</li>
<li>但完整 stream 裡有命中、只是還沒 materialize</li>
<li>使用者沒有訊號知道「還有沒抓的」</li>
</ol>
<p>三個條件同時滿足、使用者看到「filter 後是空的」、誤以為是「沒有命中」、放棄。</p>
<h3 id="為什麼這個-bug-容易寫出來">為什麼這個 bug 容易寫出來</h3>
<p>視覺層 filter 是寫起來最簡單的版本：</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="nx">items</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</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="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">title</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="o">?</span> <span class="s1">&#39;&#39;</span> <span class="o">:</span> <span class="s1">&#39;none&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>5 行解決、看起來能用、第一輪測試（手動輸入 query → 看到 filter 生效）會通過。<strong>「能用」的訊號出現太早、掩蓋了語意缺口</strong>。</p>
<p>這是 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a> 在「filter × source」情境的具體展現 — 容易寫的位置（已 materialize 的 view 層）跟對齊意圖的位置（source 層）方向相反。</p>
<hr>
<h2 id="哪些-source-形狀有層錯位風險">哪些 source 形狀有層錯位風險</h2>
<table>
  <thead>
      <tr>
          <th>Source 型態</th>
          <th>是否有層錯位風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一次性 fetch、靜態陣列</td>
          <td>否（沒有 subset）</td>
      </tr>
      <tr>
          <td>Paginated fetch（load more / cursor）</td>
          <td>是 — 本次任務的 case</td>
      </tr>
      <tr>
          <td>Streaming（SSE / WebSocket）</td>
          <td>視 server 是否限額</td>
      </tr>
      <tr>
          <td>Lazy iterator + take(N) / break</td>
          <td>是</td>
      </tr>
      <tr>
          <td>Cached + revalidate</td>
          <td>是（cache vs fresh 兩 dataset）</td>
      </tr>
  </tbody>
</table>
<p>四類 source 共用同個結構：<strong>source 分批 / 限額 / 延遲 materialize、filter 在下游 → silent 缺口</strong>。詳細形狀分析見 <a href="../data-source-shape-defines-feature-shape/">#63 資料源的形狀決定 feature 的形狀</a>。</p>
<hr>
<h2 id="這次任務的實際情境">這次任務的實際情境</h2>
<h3 id="觀察">觀察</h3>
<p>搜尋頁實作 title / content filter：</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">// pagefind 分批 load (load more 按鈕)
</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">results</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">results</span><span class="p">.</span><span class="nx">results</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">start</span><span class="p">,</span> <span class="nx">start</span> <span class="o">+</span> <span class="mi">10</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">r</span> <span class="p">=&gt;</span> <span class="nx">container</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="nx">render</span><span class="p">(</span><span class="nx">r</span><span class="p">)));</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 我們在 view 層 post-filter
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">applyFilter</span><span class="p">(</span><span class="nx">scope</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">hidden</span> <span class="o">=</span> <span class="o">!</span><span class="nx">matchesScope</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="nx">scope</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>跑出來的問題：使用者選 title-only filter、第二批 8 筆全部 title 不含 query → 點 &ldquo;load more&rdquo; 後畫面閃了一下、新增的 8 筆全 hidden、使用者看到的內容沒變。</p>
<h3 id="判讀">判讀</h3>
<p>問題的根因不在「畫面閃」這個視覺現象、而在 filter 的層級錯位：</p>
<table>
  <thead>
      <tr>
          <th>使用者意圖</th>
          <th>filter 實際對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「title 符合的」</td>
          <td>「已載入 + title 符合的」</td>
      </tr>
      <tr>
          <td>「全部結果」</td>
          <td>「已載入的全部」</td>
      </tr>
  </tbody>
</table>
<p>兩個定義在一般狀況看起來一樣（已載入子集裡有命中）、稀疏 case 暴露縫。</p>
<h3 id="執行解法選擇">執行（解法選擇）</h3>
<p>解法選擇展開見 <a href="../filter-source-composition-strategies/">#59 Filter × Source 合成策略五選一</a> — A 推進 query / B 自動續抓 / C 預先 index / D 誠實 UX / E 明示縮小。本文聚焦「先識別這是層錯位、不是 UI bug」 — 識別錯了、後續解法都會在錯誤的層上補救。</p>
<hr>
<h2 id="內在屬性比較filter-該放哪一層">內在屬性比較：filter 該放哪一層</h2>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>看到的範圍</th>
          <th>跟使用者意圖的距離</th>
          <th>寫作成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視覺層</td>
          <td>已 render 的子集</td>
          <td>最遠（差兩層）</td>
          <td>最低</td>
      </tr>
      <tr>
          <td>渲染層</td>
          <td>已 fetch 的子集</td>
          <td>中（差一層）</td>
          <td>低</td>
      </tr>
      <tr>
          <td>資料層 (源頭)</td>
          <td>完整 dataset</td>
          <td>最近</td>
          <td>中-高</td>
      </tr>
      <tr>
          <td>Source 之外</td>
          <td>重 query</td>
          <td>最近 + 最新</td>
          <td>高（query 重設計）</td>
      </tr>
  </tbody>
</table>
<p>「寫作成本最低」跟「跟意圖最近」是反相關 — 這個反相關本身是 <a href="../ease-of-writing-vs-intent-alignment/">#67</a> 的核心命題、本卡是它在 filter × source 情境的展開。</p>
<hr>
<h2 id="識別層錯位的三問">識別層錯位的三問</h2>
<p>寫 filter / sort / count / transform 之前自問：</p>
<h3 id="1-這個操作的對象是什麼層的一筆">1. 這個操作的「對象」是什麼層的「一筆」？</h3>
<p>如果寫在 view 層、對象是「螢幕上的元素」 — 那源頭如果分批、就有缺口。</p>
<h3 id="2-source-是一次給完整-dataset還是分批--限額">2. Source 是「一次給完整 dataset」還是「分批 / 限額」？</h3>
<p>對照前面「哪些 source 形狀有層錯位風險」表 — 任何分批 / 限額 / streaming / cached source 都有風險。一次性 fetch 或靜態陣列才安全。</p>
<h3 id="3-沒命中與還沒-materialize對使用者要不要區分">3. 「沒命中」與「還沒 materialize」對使用者要不要區分？</h3>
<p>要區分 → filter 必須在 source 層或自動續抓、否則使用者無法判斷。
不區分（可接受「在已載入範圍內找」這個語意） → view 層 filter 加誠實 UX。</p>
<p>三問跑完才寫 filter — 跳過任一問就可能掉進層錯位。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即將寫 <code>elements.forEach(el =&gt; el.hidden = !matches(el))</code></td>
          <td>停 — 確認 source 是不是分批的；是 → 推到資料層</td>
      </tr>
      <tr>
          <td>Source 是 <code>pagefind.search()</code> / <code>paginatedFetch()</code> / <code>for await</code> 但 filter 在 forEach</td>
          <td>是 — 重看「filter 該放哪一層」</td>
      </tr>
      <tr>
          <td>不確定 source 真實 cardinality 跟分批機制</td>
          <td>用 <a href="../playwright-early-in-loop/">#11 playwright</a> 量 live source 的回傳數量</td>
      </tr>
      <tr>
          <td>Filter 後可能 0 筆但 source 還有未載入</td>
          <td>必須補「自動續抓」或「誠實掃描範圍 UX」</td>
      </tr>
      <tr>
          <td>「Load more」「Show next」按鈕存在、且有 filter</td>
          <td>評估：filter 跟 load more 的 quota 是否同層</td>
      </tr>
      <tr>
          <td>內心 OS：「先做出來、晚點補資料層」</td>
          <td>停 — 補不回來、會 ship 進 production silent 失敗</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：filter / sort / count / transform 是 stream operation、必須跟 stream 的 materialization 同層或更上游。寫在下游 = 操作 subset 而不是 stream、語意縫是必然、不是偶發 bug。</p>
]]></content:encoded></item><item><title>視覺完成 ≠ 功能完成</title><link>https://tarrragon.github.io/blog/report/visual-completion-vs-functional-completion/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/visual-completion-vs-functional-completion/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>視覺完成是「畫面看起來對」、功能完成是「使用者意圖真的被滿足」。&lt;/strong> 兩者在簡單情境下重合、在邊界情境下分裂。視覺完成出現得早（手動 happy path 一試就過）、功能完成需要刻意對照「使用者意圖完整集合」才看得出來。&lt;/p>
&lt;p>寫程式時把「畫面對了」當成完工訊號 = 把驗收標準降到視覺層、漏掉「功能在邊界情境下是否還對」這層。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>手動視覺驗收&lt;/td>
 &lt;td>低 — 開頁、輸入一個 case&lt;/td>
 &lt;td>Happy path 的視覺正確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多 case 視覺驗收&lt;/td>
 &lt;td>中 — 想出邊界 case&lt;/td>
 &lt;td>視覺面的邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>功能對照（語意驗收）&lt;/td>
 &lt;td>高 — 列使用者意圖完整集&lt;/td>
 &lt;td>功能跟意圖之間的縫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨資料規模驗收&lt;/td>
 &lt;td>高 — 製造稀疏 / 大量資料&lt;/td>
 &lt;td>資料規模相依的功能失敗&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>成本低的訊號出現早 → 容易誤判完工。&lt;/p>
&lt;h3 id="視覺驗收的盲區">視覺驗收的盲區&lt;/h3>
&lt;p>視覺驗收只看「螢幕上呈現的」、不看「應該呈現但沒呈現的」。後者沒有視覺訊號 — 不會閃紅、不會報錯、只是「該有的東西沒出現」。&lt;/p>
&lt;p>這個盲區包括：&lt;/p>
&lt;ul>
&lt;li>Filter 把該顯示的藏掉了（見 #55 &lt;a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位&lt;/a>）&lt;/li>
&lt;li>Pagination 漏抓了某幾頁&lt;/li>
&lt;li>Sort 漏了某類元素&lt;/li>
&lt;li>Async race condition 把舊資料留在畫面&lt;/li>
&lt;/ul>
&lt;p>共通點：&lt;strong>錯誤的形式是「不該不在的不在」&lt;/strong>、不是「畫面壞了」。&lt;/p>
&lt;hr>
&lt;h2 id="多面向四類畫面對但功能漏">多面向：四類「畫面對但功能漏」&lt;/h2>
&lt;h3 id="面向-1filter--sort--count-跟-source-不同層">面向 1：Filter / Sort / Count 跟 source 不同層&lt;/h3>
&lt;p>見 #55。視覺層 filter 套在分批 source 上、稀疏 case 顯露語意縫。&lt;/p>
&lt;h3 id="面向-2async-race--競態">面向 2：Async race / 競態&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">value&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 慢的 query 後到、畫面是舊 query 的結果
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>畫面有結果、看起來對、但對應的不是當前 query。&lt;/p>
&lt;h3 id="面向-3empty-state--loading-state-不分">面向 3：Empty state / loading state 不分&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;results&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> {{ if results }}{{ for r }}{{ render r }}{{ end }}{{ end }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「還在 loading」跟「真的沒結果」共用同一個畫面 — 都是空。視覺對、功能上「使用者不知道狀態」。&lt;/p>
&lt;h3 id="面向-4form-submit-後狀態回饋失真">面向 4：Form submit 後狀態回饋失真&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">button&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">onclick&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">saveData&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="nx">button&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Saved&amp;#34;&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>按了顯示 saved、但 saveData 是 async 還沒完成 / 失敗 — 畫面對、實際資料沒進 DB。&lt;/p>
&lt;p>四個面向共用結構：&lt;strong>動作有視覺回饋、但回饋的「時機」或「對象」跟「實際語意」對不上&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="畫面對屬於哪個-checkpoint">「畫面對」屬於哪個 checkpoint&lt;/h2>
&lt;p>驗收要分散在四個時點（寫之前 / 開發中 / ship 前 / ship 後）— 詳見 &lt;a href="../verification-timeline-checkpoints/">#68 驗收的時間軸：四個 checkpoint&lt;/a>。&lt;/p>
&lt;p>「畫面對」是 &lt;strong>開發中&lt;/strong> 的視覺驗收訊號 — 用來判斷「邏輯有跑、UI 沒崩」。它&lt;strong>不能&lt;/strong>取代：&lt;/p>
&lt;ul>
&lt;li>寫之前的「意圖完整集列舉」&lt;/li>
&lt;li>Ship 前的「邊界 / 規模 case」&lt;/li>
&lt;li>Ship 後的「真實使用者紀錄」&lt;/li>
&lt;/ul>
&lt;p>把「畫面對」當完工 = 把開發中的中介訊號當終點訊號 = 跳過後三個 checkpoint。&lt;/p>
&lt;hr>
&lt;h2 id="跟-422-次門檻的關係">跟 #42「2 次門檻」的關係&lt;/h2>
&lt;p>&lt;a href="../two-occurrence-threshold/">#42 2 次門檻&lt;/a> 講「第 1 次成功是低資訊量訊號、第 2 次（同方向 / 同類）才是真訊號」。&lt;/p>
&lt;p>「畫面對」就是 #42 在「驗收訊號」面向的應用：&lt;strong>「畫面對了一次」是低資訊量訊號、跟「程式跑通一次」「測試過一次」是同類&lt;/strong>。它告訴你「至少不是完全壞的」、不告訴你「對了」。&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>跨多個 case、多個規模、跨時間後仍對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式跑通一次&lt;/td>
 &lt;td>跨多次執行、不同輸入仍跑通&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試過一次&lt;/td>
 &lt;td>涵蓋邊界 / 失敗 / 規模、CI 持續通過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者用過一次沒反映&lt;/td>
 &lt;td>多週多使用者沒累積反映&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把低資訊量訊號當完工 = 跨情境就是「同方向加碼到第 3 次」 — 都是「太早信任早期成功」的同個錯誤。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>視覺完成是「畫面看起來對」、功能完成是「使用者意圖真的被滿足」。</strong> 兩者在簡單情境下重合、在邊界情境下分裂。視覺完成出現得早（手動 happy path 一試就過）、功能完成需要刻意對照「使用者意圖完整集合」才看得出來。</p>
<p>寫程式時把「畫面對了」當成完工訊號 = 把驗收標準降到視覺層、漏掉「功能在邊界情境下是否還對」這層。</p>
<hr>
<h2 id="為什麼視覺驗收會早於功能驗收成立">為什麼視覺驗收會早於功能驗收成立</h2>
<h3 id="驗收訊號的成本梯度">驗收訊號的成本梯度</h3>
<table>
  <thead>
      <tr>
          <th>驗收方式</th>
          <th>觸發成本</th>
          <th>覆蓋的失敗類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>手動視覺驗收</td>
          <td>低 — 開頁、輸入一個 case</td>
          <td>Happy path 的視覺正確</td>
      </tr>
      <tr>
          <td>多 case 視覺驗收</td>
          <td>中 — 想出邊界 case</td>
          <td>視覺面的邊界</td>
      </tr>
      <tr>
          <td>功能對照（語意驗收）</td>
          <td>高 — 列使用者意圖完整集</td>
          <td>功能跟意圖之間的縫</td>
      </tr>
      <tr>
          <td>跨資料規模驗收</td>
          <td>高 — 製造稀疏 / 大量資料</td>
          <td>資料規模相依的功能失敗</td>
      </tr>
  </tbody>
</table>
<p>成本低的訊號出現早 → 容易誤判完工。</p>
<h3 id="視覺驗收的盲區">視覺驗收的盲區</h3>
<p>視覺驗收只看「螢幕上呈現的」、不看「應該呈現但沒呈現的」。後者沒有視覺訊號 — 不會閃紅、不會報錯、只是「該有的東西沒出現」。</p>
<p>這個盲區包括：</p>
<ul>
<li>Filter 把該顯示的藏掉了（見 #55 <a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位</a>）</li>
<li>Pagination 漏抓了某幾頁</li>
<li>Sort 漏了某類元素</li>
<li>Async race condition 把舊資料留在畫面</li>
</ul>
<p>共通點：<strong>錯誤的形式是「不該不在的不在」</strong>、不是「畫面壞了」。</p>
<hr>
<h2 id="多面向四類畫面對但功能漏">多面向：四類「畫面對但功能漏」</h2>
<h3 id="面向-1filter--sort--count-跟-source-不同層">面向 1：Filter / Sort / Count 跟 source 不同層</h3>
<p>見 #55。視覺層 filter 套在分批 source 上、稀疏 case 顯露語意縫。</p>
<h3 id="面向-2async-race--競態">面向 2：Async race / 競態</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="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="kr">async</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">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">search</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">render</span><span class="p">(</span><span class="nx">r</span><span class="p">);</span>  <span class="c1">// 慢的 query 後到、畫面是舊 query 的結果
</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>畫面有結果、看起來對、但對應的不是當前 query。</p>
<h3 id="面向-3empty-state--loading-state-不分">面向 3：Empty state / loading state 不分</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;results&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  {{ if results }}{{ for r }}{{ render r }}{{ end }}{{ end }}
</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></code></pre></div><p>「還在 loading」跟「真的沒結果」共用同一個畫面 — 都是空。視覺對、功能上「使用者不知道狀態」。</p>
<h3 id="面向-4form-submit-後狀態回饋失真">面向 4：Form submit 後狀態回饋失真</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="nx">button</span><span class="p">.</span><span class="nx">onclick</span> <span class="o">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">saveData</span><span class="p">();</span> <span class="nx">button</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s2">&#34;Saved&#34;</span><span class="p">;</span> <span class="p">};</span></span></span></code></pre></div><p>按了顯示 saved、但 saveData 是 async 還沒完成 / 失敗 — 畫面對、實際資料沒進 DB。</p>
<p>四個面向共用結構：<strong>動作有視覺回饋、但回饋的「時機」或「對象」跟「實際語意」對不上</strong>。</p>
<hr>
<h2 id="畫面對屬於哪個-checkpoint">「畫面對」屬於哪個 checkpoint</h2>
<p>驗收要分散在四個時點（寫之前 / 開發中 / ship 前 / ship 後）— 詳見 <a href="../verification-timeline-checkpoints/">#68 驗收的時間軸：四個 checkpoint</a>。</p>
<p>「畫面對」是 <strong>開發中</strong> 的視覺驗收訊號 — 用來判斷「邏輯有跑、UI 沒崩」。它<strong>不能</strong>取代：</p>
<ul>
<li>寫之前的「意圖完整集列舉」</li>
<li>Ship 前的「邊界 / 規模 case」</li>
<li>Ship 後的「真實使用者紀錄」</li>
</ul>
<p>把「畫面對」當完工 = 把開發中的中介訊號當終點訊號 = 跳過後三個 checkpoint。</p>
<hr>
<h2 id="跟-422-次門檻的關係">跟 #42「2 次門檻」的關係</h2>
<p><a href="../two-occurrence-threshold/">#42 2 次門檻</a> 講「第 1 次成功是低資訊量訊號、第 2 次（同方向 / 同類）才是真訊號」。</p>
<p>「畫面對」就是 #42 在「驗收訊號」面向的應用：<strong>「畫面對了一次」是低資訊量訊號、跟「程式跑通一次」「測試過一次」是同類</strong>。它告訴你「至少不是完全壞的」、不告訴你「對了」。</p>
<table>
  <thead>
      <tr>
          <th>低資訊量訊號</th>
          <th>真訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>畫面對了一次</td>
          <td>跨多個 case、多個規模、跨時間後仍對</td>
      </tr>
      <tr>
          <td>程式跑通一次</td>
          <td>跨多次執行、不同輸入仍跑通</td>
      </tr>
      <tr>
          <td>測試過一次</td>
          <td>涵蓋邊界 / 失敗 / 規模、CI 持續通過</td>
      </tr>
      <tr>
          <td>使用者用過一次沒反映</td>
          <td>多週多使用者沒累積反映</td>
      </tr>
  </tbody>
</table>
<p>把低資訊量訊號當完工 = 跨情境就是「同方向加碼到第 3 次」 — 都是「太早信任早期成功」的同個錯誤。</p>
<hr>
<h2 id="識別視覺完成但功能未完成的訊號">識別「視覺完成但功能未完成」的訊號</h2>
<h3 id="訊號-1驗收靠再點一下試試">訊號 1：驗收靠「再點一下試試」</h3>
<p>如果發現 bug 的方式是「我再操作一次就看出來了」 — 表示 happy path 過了、邊界 case 沒過。看到這個訊號要主動列邊界 case。</p>
<h3 id="訊號-2使用者描述的-bug-含有時候偶爾我以為">訊號 2：使用者描述的 bug 含「有時候」「偶爾」「我以為」</h3>
<p>「有時候 load more 沒動」「我以為都篩過了」 — 這類語言反映的是「畫面跟意圖之間有縫、使用者用視覺驗收結果跟意圖對不上」。</p>
<h3 id="訊號-3實作時下意識覺得先這樣晚點補">訊號 3：實作時下意識覺得「先這樣、晚點補」</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="c1">// TODO: 處理 cache 跟 fresh 的合併
</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">data</span> <span class="o">=</span> <span class="nx">cached</span> <span class="o">||</span> <span class="nx">fresh</span><span class="p">;</span></span></span></code></pre></div><p>「晚點補」的部分通常就是視覺看不見的功能缺口。如果視覺驗收會過、TODO 會被忘記到 production。</p>
<h3 id="訊號-4測試只有-happy-path-截圖">訊號 4：測試只有 happy path 截圖</h3>
<p>PR / commit 附的截圖只有「最常見的 case」 — 沒有「沒結果」「載入中」「失敗」「資料規模特別大 / 特別小」的截圖 → 驗收層級停在視覺。</p>
<hr>
<h2 id="設計取捨怎麼把驗收從視覺升到功能">設計取捨：怎麼把驗收從視覺升到功能</h2>
<p>四種做法、不同情境合理。</p>
<h3 id="a寫之前列使用者意圖的完整-case-集合實作後逐一對照">A：寫之前列「使用者意圖的完整 case 集合」、實作後逐一對照</h3>
<ul>
<li><strong>機制</strong>：開工前列 happy path / 邊界 case / 失敗 case 三類、實作完逐一檢查</li>
<li><strong>選 A 的理由</strong>：把驗收標準從「能用」升到「對齊意圖」</li>
<li><strong>代價</strong>：需要主動想 case、寫之前花時間</li>
</ul>
<h3 id="b靠自動化測試unit--e2e覆蓋邊界">B：靠自動化測試（unit / e2e）覆蓋邊界</h3>
<ul>
<li><strong>機制</strong>：每個 case 寫一個測試、CI 跑</li>
<li><strong>跟 A 的取捨</strong>：B 持續性更好、但成本高、且測試是寫的人決定的、漏想 case 一樣會漏</li>
<li><strong>B 才合理的情境</strong>：大專案、團隊協作、回歸風險高</li>
</ul>
<h3 id="c靠使用者回報">C：靠使用者回報</h3>
<ul>
<li><strong>機制</strong>：先 ship、使用者反映再修</li>
<li><strong>跟 A 的取捨</strong>：C 工程量最低、但 trust 損失高、bug 進 production 才被發現</li>
<li><strong>C 才合理的情境</strong>：原型期、使用者願意幫忙找 bug、易回滾</li>
</ul>
<h3 id="d只做視覺驗收反模式">D：只做視覺驗收（反模式）</h3>
<ul>
<li><strong>為什麼是反模式</strong>：把驗收標準降到視覺層、漏掉「功能跟意圖之間的縫」這層 — 而那層的失敗最常見也最貴</li>
<li><strong>看起來吸引人的原因</strong>：成本最低、happy path 過了就 OK、不需要列邊界 case</li>
<li><strong>實際發生的代價</strong>：silent 缺口累積、系統性使用者不信任、ship 後發現修起來比早期貴 N 倍（見 <a href="../verification-timeline-checkpoints/#%e7%80%91%e5%b8%83%e5%8e%9f%e5%89%87%e6%bc%8f%e4%b8%80%e5%b1%a4%e4%bb%a3%e5%83%b9%e6%8c%87%e6%95%b8%e6%94%be%e5%a4%a7">#68 瀑布原則</a>）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>驗收只看了 happy path 截圖</td>
          <td>補：邊界 case + 失敗 case + 規模 case</td>
      </tr>
      <tr>
          <td>內心 OS：「畫面對了應該就 OK」</td>
          <td>停 — 列「使用者意圖完整集合」對照</td>
      </tr>
      <tr>
          <td>Bug report 含「有時候」「偶爾」「我以為」</td>
          <td>是「畫面跟意圖之間有縫」的訊號</td>
      </tr>
      <tr>
          <td>實作時寫了 TODO 但視覺驗收會過</td>
          <td>TODO 會在 production 被遺忘、必須補完</td>
      </tr>
      <tr>
          <td>Filter / sort / async / cache 等「狀態相依」的功能完成</td>
          <td>主動跑「規模 / 稀疏 / 競態」三類 case</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：視覺驗收是必要、不是充分。功能驗收要對照「使用者意圖完整集合」、不只是「畫面對」。視覺對 + 意圖縫 = 比畫面壞更危險、因為它不會觸發任何訊號。</p>
<p>延伸到測試驗收：「測試 PASS」也是視覺訊號的同類 — 沒看過該測試 RED 過、不知道它有沒有 catch 能力。詳見 <a href="../test-first-red-before-green/">#69 Test-First：先看到 RED 才相信 GREEN</a>。</p>
]]></content:encoded></item><item><title>Loading / Empty / End 三狀態的區分</title><link>https://tarrragon.github.io/blog/report/loading-empty-end-state-distinction/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/loading-empty-end-state-distinction/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>「Loading」「Empty」「End」是三個語意不同的狀態、UX 必須區分。&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>Loading&lt;/td>
 &lt;td>還在抓、結果未知&lt;/td>
 &lt;td>等&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Empty&lt;/td>
 &lt;td>抓完了、確認無命中&lt;/td>
 &lt;td>改 query / 改 filter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>End&lt;/td>
 &lt;td>抓完了、有結果但無更多&lt;/td>
 &lt;td>看當前結果、不要再 load more&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>混為一談 = 使用者該等的時候改 query、該改 query 的時候等、該停的時候繼續點 load more。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Loading&lt;/td>
 &lt;td>空白 + spinner&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Empty&lt;/td>
 &lt;td>空白 + 「無結果」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>End&lt;/td>
 &lt;td>結果 + 灰掉的按鈕&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Loading 跟 Empty 都是「空白為底」、容易共用畫面。實作時如果只寫 &lt;code>{{ if results }}...{{ else }}&amp;lt;empty /&amp;gt;{{ end }}&lt;/code>、Loading 跟 Empty 會被當成同一件事。&lt;/p>
&lt;h3 id="資料層常常沒提供區分訊號">資料層常常沒提供區分訊號&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetch&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">showEmpty&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>r.length === 0&lt;/code> 只區分有 / 無、不區分「為什麼無」。要區分「還沒抓」vs「抓完無命中」、需要顯式追蹤 fetch 的狀態（pending / done / error），不是看 result。&lt;/p>
&lt;p>End 狀態類似：&lt;code>results.length &amp;gt; 0 &amp;amp;&amp;amp; !hasMore&lt;/code> 才是 End、跟「還可以 load more 的當前結果」不同。&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>Loading&lt;/td>
 &lt;td>&lt;code>fetchState === 'pending'&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Empty&lt;/td>
 &lt;td>&lt;code>fetchState === 'done' &amp;amp;&amp;amp; results.length === 0&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>End&lt;/td>
 &lt;td>&lt;code>fetchState === 'done' &amp;amp;&amp;amp; results.length &amp;gt; 0 &amp;amp;&amp;amp; !hasMore&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實作上至少需要：&lt;/p>
&lt;ul>
&lt;li>一個 fetch state machine（不能只看 &lt;code>results&lt;/code>）&lt;/li>
&lt;li>一個「還有沒有下一批」的訊號（&lt;code>hasMore&lt;/code> / cursor / total count）&lt;/li>
&lt;li>UI 對三種組合各畫一個樣子&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="多面向三狀態的延伸">多面向：三狀態的延伸&lt;/h2>
&lt;h3 id="面向-1filter-加進來狀態空間擴張">面向 1：Filter 加進來、狀態空間擴張&lt;/h3>
&lt;p>當 view 層有 filter、三狀態擴張為五狀態（Loading / Empty-raw / Empty-filter / Partial / End）。「Empty-filter」跟「Partial」是 &lt;a href="../view-layer-filter-vs-source-layer/">#55 層錯位&lt;/a> 的 UX 表現 — 共用同個 empty 畫面 = 使用者無法判斷「再 load more 會不會有」。&lt;/p>
&lt;p>具體 UX 模板（三數字、五狀態各別 UI）見 &lt;a href="../pattern-honest-progress-ui/">#62 Pattern：誠實進度 UX&lt;/a>。&lt;/p>
&lt;h3 id="面向-2streaming--sse-的無更多很難判斷">面向 2：Streaming / SSE 的「無更多」很難判斷&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">item&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="nx">eventSource&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&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">2&lt;/span>&lt;span class="cl">&lt;span class="c1">// 跑完了還是斷線了？
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Streaming 通常沒明確的 End 訊號 — 需要 server 主動送一個 &lt;code>event: end&lt;/code>、或 client 用 timeout / heartbeat 判斷。否則使用者看到一段時間沒新資料、不知道是「沒了」還是「還在等」。&lt;/p>
&lt;h3 id="面向-3錯誤狀態應該獨立不混進三狀態">面向 3：錯誤狀態應該獨立、不混進三狀態&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>Error&lt;/td>
 &lt;td>獨立第四個狀態、需要不同 UX&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Timeout&lt;/td>
 &lt;td>通常歸 Error&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Offline&lt;/td>
 &lt;td>獨立、需要 retry UX&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把 Error 顯示成 Empty = 使用者誤以為「沒結果」、不會 retry。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>「Loading」「Empty」「End」是三個語意不同的狀態、UX 必須區分。</strong> 三者在資料層代表完全不同的事實、使用者根據哪一個決定下一步動作；共用畫面 = 使用者沒辦法決定。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>資料層事實</th>
          <th>使用者該採取的下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loading</td>
          <td>還在抓、結果未知</td>
          <td>等</td>
      </tr>
      <tr>
          <td>Empty</td>
          <td>抓完了、確認無命中</td>
          <td>改 query / 改 filter</td>
      </tr>
      <tr>
          <td>End</td>
          <td>抓完了、有結果但無更多</td>
          <td>看當前結果、不要再 load more</td>
      </tr>
  </tbody>
</table>
<p>混為一談 = 使用者該等的時候改 query、該改 query 的時候等、該停的時候繼續點 load more。</p>
<hr>
<h2 id="為什麼三狀態容易被混為一談">為什麼三狀態容易被混為一談</h2>
<h3 id="視覺上類似">視覺上類似</h3>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>常見視覺</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loading</td>
          <td>空白 + spinner</td>
      </tr>
      <tr>
          <td>Empty</td>
          <td>空白 + 「無結果」</td>
      </tr>
      <tr>
          <td>End</td>
          <td>結果 + 灰掉的按鈕</td>
      </tr>
  </tbody>
</table>
<p>Loading 跟 Empty 都是「空白為底」、容易共用畫面。實作時如果只寫 <code>{{ if results }}...{{ else }}&lt;empty /&gt;{{ end }}</code>、Loading 跟 Empty 會被當成同一件事。</p>
<h3 id="資料層常常沒提供區分訊號">資料層常常沒提供區分訊號</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="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="nx">showEmpty</span><span class="p">();</span></span></span></code></pre></div><p><code>r.length === 0</code> 只區分有 / 無、不區分「為什麼無」。要區分「還沒抓」vs「抓完無命中」、需要顯式追蹤 fetch 的狀態（pending / done / error），不是看 result。</p>
<p>End 狀態類似：<code>results.length &gt; 0 &amp;&amp; !hasMore</code> 才是 End、跟「還可以 load more 的當前結果」不同。</p>
<hr>
<h2 id="三狀態的可區分訊號">三狀態的可區分訊號</h2>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>必要訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loading</td>
          <td><code>fetchState === 'pending'</code></td>
      </tr>
      <tr>
          <td>Empty</td>
          <td><code>fetchState === 'done' &amp;&amp; results.length === 0</code></td>
      </tr>
      <tr>
          <td>End</td>
          <td><code>fetchState === 'done' &amp;&amp; results.length &gt; 0 &amp;&amp; !hasMore</code></td>
      </tr>
  </tbody>
</table>
<p>實作上至少需要：</p>
<ul>
<li>一個 fetch state machine（不能只看 <code>results</code>）</li>
<li>一個「還有沒有下一批」的訊號（<code>hasMore</code> / cursor / total count）</li>
<li>UI 對三種組合各畫一個樣子</li>
</ul>
<hr>
<h2 id="多面向三狀態的延伸">多面向：三狀態的延伸</h2>
<h3 id="面向-1filter-加進來狀態空間擴張">面向 1：Filter 加進來、狀態空間擴張</h3>
<p>當 view 層有 filter、三狀態擴張為五狀態（Loading / Empty-raw / Empty-filter / Partial / End）。「Empty-filter」跟「Partial」是 <a href="../view-layer-filter-vs-source-layer/">#55 層錯位</a> 的 UX 表現 — 共用同個 empty 畫面 = 使用者無法判斷「再 load more 會不會有」。</p>
<p>具體 UX 模板（三數字、五狀態各別 UI）見 <a href="../pattern-honest-progress-ui/">#62 Pattern：誠實進度 UX</a>。</p>
<h3 id="面向-2streaming--sse-的無更多很難判斷">面向 2：Streaming / SSE 的「無更多」很難判斷</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="k">for</span> <span class="kr">await</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">item</span> <span class="k">of</span> <span class="nx">eventSource</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 跑完了還是斷線了？
</span></span></span></code></pre></div><p>Streaming 通常沒明確的 End 訊號 — 需要 server 主動送一個 <code>event: end</code>、或 client 用 timeout / heartbeat 判斷。否則使用者看到一段時間沒新資料、不知道是「沒了」還是「還在等」。</p>
<h3 id="面向-3錯誤狀態應該獨立不混進三狀態">面向 3：錯誤狀態應該獨立、不混進三狀態</h3>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>跟三狀態的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Error</td>
          <td>獨立第四個狀態、需要不同 UX</td>
      </tr>
      <tr>
          <td>Timeout</td>
          <td>通常歸 Error</td>
      </tr>
      <tr>
          <td>Offline</td>
          <td>獨立、需要 retry UX</td>
      </tr>
  </tbody>
</table>
<p>把 Error 顯示成 Empty = 使用者誤以為「沒結果」、不會 retry。</p>
<hr>
<h2 id="設計取捨ux-該怎麼呈現三狀態">設計取捨：UX 該怎麼呈現三狀態</h2>
<h3 id="a每個狀態獨立的-ui-元件">A：每個狀態獨立的 UI 元件</h3>
<ul>
<li><strong>機制</strong>：Loading 顯示 spinner、Empty 顯示 illustration + 「改 query」CTA、End 顯示「all results loaded」、Error 顯示 retry button</li>
<li><strong>選 A 的理由</strong>：四個狀態語意完全清楚、使用者下一步明確</li>
<li><strong>代價</strong>：UI 元件多、設計成本高</li>
</ul>
<h3 id="b用文字--細節區分共用-layout">B：用文字 + 細節區分、共用 layout</h3>
<ul>
<li><strong>機制</strong>：同一個 container、不同狀態填不同文字（&ldquo;Loading&hellip;&rdquo; / &ldquo;No results for X&rdquo; / &ldquo;Showing all 23 results&rdquo;）</li>
<li><strong>跟 A 的取捨</strong>：B 設計簡單、但區分性弱（使用者要讀文字才知道狀態）</li>
<li><strong>B 才合理的情境</strong>：簡單 UI、使用者願意讀文字</li>
</ul>
<h3 id="c只用視覺-cuespinner--空白">C：只用視覺 cue（spinner / 空白）</h3>
<ul>
<li><strong>機制</strong>：spinner = loading、空白 = 沒結果、結果列表 = 有</li>
<li><strong>跟 A 的取捨</strong>：C 沒區分 Empty vs End vs Partial</li>
<li><strong>C 才合理的情境</strong>：source 沒分批、結果一次給完</li>
</ul>
<h3 id="d完全不區分三狀態反模式">D：完全不區分三狀態（反模式）</h3>
<ul>
<li><strong>為什麼是反模式</strong>：把「使用者下一步該做什麼」這個決策丟給使用者自己猜、違反「UI 必須回答下一步問題」原則</li>
<li><strong>看起來吸引人的原因</strong>：UI 寫起來最簡單、不用畫 Loading / Empty / End 三版、<code>{{ if results }}...{{ else }}empty{{ end }}</code> 一行解決</li>
<li><strong>實際發生的代價</strong>：使用者操作不知所措、support tickets 增加、使用者信任損失（「這網站到底有沒有在 load」）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>UI 寫 <code>{{ if results }}...{{ else }}&lt;empty /&gt;{{ end }}</code></td>
          <td>補：Loading / Error / End / Partial 各一個分支</td>
      </tr>
      <tr>
          <td>沒有 <code>fetchState</code> / <code>hasMore</code> 變數</td>
          <td>加 — 否則無法區分三狀態</td>
      </tr>
      <tr>
          <td>Empty UI 上沒有「下一步該做什麼」的 CTA</td>
          <td>補：「改 query」「reset filter」「retry」等行動建議</td>
      </tr>
      <tr>
          <td>Loading 共用 Empty 畫面（都是空白）</td>
          <td>加區分（spinner vs 文字）</td>
      </tr>
      <tr>
          <td>Streaming / async iterator 沒明確 End 訊號</td>
          <td>加：server-side 送 end event、或 client timeout</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：三狀態（Loading / Empty / End）是不同事實、不同 UX。共用畫面 = 把「使用者該做什麼」這個決策丟給使用者自己猜。實作要從資料層追蹤 state、不能只看 <code>results</code>。</p>
<p>跟 <a href="../aria-live-for-dynamic-content/">#38 動態內容變動的 aria-live region 設計</a> 同源：兩者都是「狀態變動需要告知使用者」、本卡告訴的是 sighted 使用者（視覺區分）、#38 告訴 screen reader（aria-live 廣播）。</p>
]]></content:encoded></item><item><title>篩選類指令的澄清時機</title><link>https://tarrragon.github.io/blog/report/filter-instruction-clarification/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/filter-instruction-clarification/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>「依 X 篩選」這類指令、寫之前必須澄清三件事&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>篩選的&lt;strong>定義域&lt;/strong>是「已載入的子集」還是「全部結果」？&lt;/li>
&lt;li>資料源是&lt;strong>一次性給完整 dataset&lt;/strong> 還是&lt;strong>分批 / 限額&lt;/strong>？&lt;/li>
&lt;li>「沒命中」與「還沒抓到」要不要在 UI 上&lt;strong>區分&lt;/strong>？&lt;/li>
&lt;/ol>
&lt;p>三問沒跑完就直接寫、必然寫成視覺層 post-filter（最容易實作的版本）、撞上 #55 &lt;a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位&lt;/a>。&lt;/p>
&lt;p>這是 &lt;a href="../decide-vs-confirm-boundary/">#16-23 第三輪指令澄清&lt;/a> 的第 5 類：篩選類指令。&lt;/p>
&lt;p>跟前四類的差別：&lt;/p>
&lt;ul>
&lt;li>空間 / 位置 / 隔離類（#16-#18）— 缺的是&lt;strong>幾何資訊&lt;/strong>（數字、layout、邊界）&lt;/li>
&lt;li>決定權類（#21）— 缺的是&lt;strong>誰拍板&lt;/strong>（visible 三問）&lt;/li>
&lt;li>篩選類（本卡）— 缺的是&lt;strong>操作的層級&lt;/strong>（filter 的 stream 範圍 = 哪一層的「一筆」）&lt;/li>
&lt;/ul>
&lt;p>前四類的澄清能避免實作走錯方向、第 5 類能避免架構上錯層（#55 層錯位）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼篩選指令需要獨立的澄清協議">為什麼篩選指令需要獨立的澄清協議&lt;/h2>
&lt;p>「依 X 篩選」的指令在使用者口中是一個簡單訴求、在實作上有兩個獨立決策：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>語意決策&lt;/strong>：filter 的定義域是哪一層？&lt;/li>
&lt;li>&lt;strong>UX 決策&lt;/strong>：邊界 state（loading / empty / partial）怎麼呈現？&lt;/li>
&lt;/ul>
&lt;p>兩個決策不澄清、執行者預設選最簡單版本（view 層 + silent fail）— 但這版本對應的使用者意圖通常不是使用者真的要的。&lt;/p>
&lt;hr>
&lt;h2 id="三問模板">三問模板&lt;/h2>
&lt;h3 id="問-1定義域">問 1：定義域&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">「依 X 篩選」是指：
&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">(a) 在已載入的結果裡找 X 符合的（filter 範圍 = 已抓的子集）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">(b) 在所有結果裡找 X 符合的（filter 範圍 = 完整 dataset）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">(c) 重新搜尋、把 X 當成 query 條件（filter ≡ 改 query）
&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">通常 (b) 是使用者預期、但實作成本看 (c) 是不是 source 支援的。哪一個？&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回答決定 filter 該寫在哪一層（見 #55）。&lt;/p>
&lt;h3 id="問-2資料源型態">問 2：資料源型態&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">資料源是：
&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">(a) 一次性給完整 dataset（靜態陣列、一次 fetch 到底）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">(b) 分批 / 限額（pagefind、paginated API、infinite scroll）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">(c) Streaming（SSE / WebSocket、來多少看多少）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">(d) Cached + revalidate（先 cache 後 fresh）
&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">(a) 沒有層錯位風險、直接寫 view 層 filter；
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">(b)(c)(d) 必須跟 source 對齊或加自動續抓 / 誠實 UX。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>問 1 跟問 2 的組合決定實作模式：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;/th>
 &lt;th>Source (a) 一次性&lt;/th>
 &lt;th>Source (b)(c)(d) 分批&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>定義域 (a)&lt;/td>
 &lt;td>view filter OK&lt;/td>
 &lt;td>view filter + 誠實 UX&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>定義域 (b)&lt;/td>
 &lt;td>view filter OK&lt;/td>
 &lt;td>自動續抓、或 push 到 query&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>定義域 (c)&lt;/td>
 &lt;td>改 query&lt;/td>
 &lt;td>改 query&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="問-3空狀態區分">問 3：空狀態區分&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">當 filter 後 0 筆顯示、要不要區分：
&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">(a) 「沒命中」（已抓完、確定 0）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">(b) 「還沒抓到」（已載入子集裡 0、source 還有）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">(c) 「載入中」（fetch 還在跑）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">(d) 「載入失敗」
&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">通常 (a)(b)(c)(d) 都該區分（見 #57 三狀態）、但實作上能忍受多少混為一談？&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>回答決定 UX 要做到多細（見 #57 &lt;a href="../loading-empty-end-state-distinction/">Loading / Empty / End 三狀態的區分&lt;/a>）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>「依 X 篩選」這類指令、寫之前必須澄清三件事</strong>：</p>
<ol>
<li>篩選的<strong>定義域</strong>是「已載入的子集」還是「全部結果」？</li>
<li>資料源是<strong>一次性給完整 dataset</strong> 還是<strong>分批 / 限額</strong>？</li>
<li>「沒命中」與「還沒抓到」要不要在 UI 上<strong>區分</strong>？</li>
</ol>
<p>三問沒跑完就直接寫、必然寫成視覺層 post-filter（最容易實作的版本）、撞上 #55 <a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位</a>。</p>
<p>這是 <a href="../decide-vs-confirm-boundary/">#16-23 第三輪指令澄清</a> 的第 5 類：篩選類指令。</p>
<p>跟前四類的差別：</p>
<ul>
<li>空間 / 位置 / 隔離類（#16-#18）— 缺的是<strong>幾何資訊</strong>（數字、layout、邊界）</li>
<li>決定權類（#21）— 缺的是<strong>誰拍板</strong>（visible 三問）</li>
<li>篩選類（本卡）— 缺的是<strong>操作的層級</strong>（filter 的 stream 範圍 = 哪一層的「一筆」）</li>
</ul>
<p>前四類的澄清能避免實作走錯方向、第 5 類能避免架構上錯層（#55 層錯位）。</p>
<hr>
<h2 id="為什麼篩選指令需要獨立的澄清協議">為什麼篩選指令需要獨立的澄清協議</h2>
<p>「依 X 篩選」的指令在使用者口中是一個簡單訴求、在實作上有兩個獨立決策：</p>
<ul>
<li><strong>語意決策</strong>：filter 的定義域是哪一層？</li>
<li><strong>UX 決策</strong>：邊界 state（loading / empty / partial）怎麼呈現？</li>
</ul>
<p>兩個決策不澄清、執行者預設選最簡單版本（view 層 + silent fail）— 但這版本對應的使用者意圖通常不是使用者真的要的。</p>
<hr>
<h2 id="三問模板">三問模板</h2>
<h3 id="問-1定義域">問 1：定義域</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">「依 X 篩選」是指：
</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">(a) 在已載入的結果裡找 X 符合的（filter 範圍 = 已抓的子集）
</span></span><span class="line"><span class="ln">4</span><span class="cl">(b) 在所有結果裡找 X 符合的（filter 範圍 = 完整 dataset）
</span></span><span class="line"><span class="ln">5</span><span class="cl">(c) 重新搜尋、把 X 當成 query 條件（filter ≡ 改 query）
</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">通常 (b) 是使用者預期、但實作成本看 (c) 是不是 source 支援的。哪一個？</span></span></code></pre></div><p>回答決定 filter 該寫在哪一層（見 #55）。</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">資料源是：
</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">(a) 一次性給完整 dataset（靜態陣列、一次 fetch 到底）
</span></span><span class="line"><span class="ln">4</span><span class="cl">(b) 分批 / 限額（pagefind、paginated API、infinite scroll）
</span></span><span class="line"><span class="ln">5</span><span class="cl">(c) Streaming（SSE / WebSocket、來多少看多少）
</span></span><span class="line"><span class="ln">6</span><span class="cl">(d) Cached + revalidate（先 cache 後 fresh）
</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">(a) 沒有層錯位風險、直接寫 view 層 filter；
</span></span><span class="line"><span class="ln">9</span><span class="cl">(b)(c)(d) 必須跟 source 對齊或加自動續抓 / 誠實 UX。</span></span></code></pre></div><p>問 1 跟問 2 的組合決定實作模式：</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Source (a) 一次性</th>
          <th>Source (b)(c)(d) 分批</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>定義域 (a)</td>
          <td>view filter OK</td>
          <td>view filter + 誠實 UX</td>
      </tr>
      <tr>
          <td>定義域 (b)</td>
          <td>view filter OK</td>
          <td>自動續抓、或 push 到 query</td>
      </tr>
      <tr>
          <td>定義域 (c)</td>
          <td>改 query</td>
          <td>改 query</td>
      </tr>
  </tbody>
</table>
<h3 id="問-3空狀態區分">問 3：空狀態區分</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">當 filter 後 0 筆顯示、要不要區分：
</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">(a) 「沒命中」（已抓完、確定 0）
</span></span><span class="line"><span class="ln">4</span><span class="cl">(b) 「還沒抓到」（已載入子集裡 0、source 還有）
</span></span><span class="line"><span class="ln">5</span><span class="cl">(c) 「載入中」（fetch 還在跑）
</span></span><span class="line"><span class="ln">6</span><span class="cl">(d) 「載入失敗」
</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">通常 (a)(b)(c)(d) 都該區分（見 #57 三狀態）、但實作上能忍受多少混為一談？</span></span></code></pre></div><p>回答決定 UX 要做到多細（見 #57 <a href="../loading-empty-end-state-distinction/">Loading / Empty / End 三狀態的區分</a>）。</p>
<hr>
<h2 id="多面向篩選指令的不同形式">多面向：篩選指令的不同形式</h2>
<h3 id="形式-1boolean-篩選">形式 1：Boolean 篩選</h3>
<p>「只看標記為 favorite 的」「只看 type = post」 — 屬性匹配、二元。</p>
<p>通常推到 query 層（formula: <code>type = post</code>）、不在 view 層 hide。</p>
<h3 id="形式-2substring--regex-篩選">形式 2：Substring / regex 篩選</h3>
<p>「title 含 X」「內文匹配某 pattern」 — 字串搜尋、可能跨欄位。</p>
<p>如果 source 有 full-text index、推到 query；沒有 → 自動續抓 + 應用 regex。</p>
<h3 id="形式-3範圍篩選">形式 3：範圍篩選</h3>
<p>「日期在 X-Y 之間」「分數 &gt; 80」 — 連續值區間。</p>
<p>通常 source 支援（SQL <code>BETWEEN</code>、API 的 ?from=&amp;to=）、推到 query。</p>
<h3 id="形式-4facet多選交集">形式 4：Facet（多選交集）</h3>
<p>「type=post AND tag=js AND date&gt;2024」 — 多條件組合。</p>
<p>實作通常是 source 支援多 filter 參數、UI 提供 facet 介面。每個 facet 獨立澄清三問。</p>
<h3 id="形式-5客製計算後篩選">形式 5：客製計算後篩選</h3>
<p>「閱讀時間 &gt; 5 分鐘」「distance &lt; 1 km」 — 需要計算、source 通常不會直接支援。</p>
<p>要嘛預先計算後存到 source（推到 query）、要嘛接受「在已載入子集裡計算」的語意縮小。</p>
<hr>
<h2 id="設計取捨澄清的時機與形式">設計取捨：澄清的時機與形式</h2>
<h3 id="a寫第一行-code-前澄清三問">A：寫第一行 code 前澄清三問</h3>
<ul>
<li><strong>機制</strong>：使用者下指令、執行者立刻列三問、給三問的選項、讓使用者選</li>
<li><strong>選 A 的理由</strong>：避免錯實作、避免層錯位</li>
<li><strong>代價</strong>：對話成本中（三問 + 選項）</li>
</ul>
<h3 id="b邊寫邊發現邊問">B：邊寫邊發現邊問</h3>
<ul>
<li><strong>機制</strong>：先寫 view 層、發現邊界 case 不對再回問</li>
<li><strong>跟 A 的取捨</strong>：B 對話成本看起來低、但累積的重做成本高（架構已選錯方向）</li>
<li><strong>B 才合理的情境</strong>：原型 / 探索期、選錯架構成本低</li>
</ul>
<h3 id="c執行者自己選一個版本commit看使用者驗收">C：執行者自己選一個版本、commit、看使用者驗收</h3>
<ul>
<li><strong>跟 A 的取捨</strong>：C 把分析丟給使用者驗收、把使用者意圖跟實作不匹配的成本後置</li>
<li><strong>C 才合理的情境</strong>：執行者跟使用者是同一人、或預期會多輪迭代</li>
</ul>
<h3 id="d忽略澄清直接寫-view-層-silent-post-filter反模式">D：忽略澄清、直接寫 view 層 silent post-filter（反模式）</h3>
<ul>
<li><strong>為什麼是反模式</strong>：寫出的版本對應的不是使用者意圖、撞上 <a href="../view-layer-filter-vs-source-layer/">#55 層錯位</a>、ship 後 silent 失敗</li>
<li><strong>看起來吸引人的原因</strong>：對話成本看起來最低（不用問三問）、5 行 forEach 解決</li>
<li><strong>實際發生的代價</strong>：使用者誤以為「沒命中」、放棄使用、得不到回報（silent 失敗最隱蔽）</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>收到「依 X 篩選」「只看 X」「過濾 Y」這類指令</td>
          <td>跑三問、列選項</td>
      </tr>
      <tr>
          <td>即將寫 <code>elements.forEach(el =&gt; el.hidden = !matches(el))</code></td>
          <td>三問先跑</td>
      </tr>
      <tr>
          <td>Source 是分批的、且使用者沒明示「filter 範圍」</td>
          <td>必問問 1 — 定義域</td>
      </tr>
      <tr>
          <td>Filter 後可能 0 筆、且使用者沒明示「沒命中 vs 還沒抓到」要不要區分</td>
          <td>必問問 3 — 空狀態</td>
      </tr>
      <tr>
          <td>內心 OS：「先做 view 層、晚點補資料層」</td>
          <td>停 — 跑完三問、確認方向再寫</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：篩選指令的「簡單表面」掩蓋了三個獨立決策。澄清三問是必要、不是過度溝通 — 跳過任一問就會寫成「能用但跟意圖有縫」的版本。</p>
]]></content:encoded></item><item><title>Filter × Source 的合成策略五選一</title><link>https://tarrragon.github.io/blog/report/filter-source-composition-strategies/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/filter-source-composition-strategies/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Filter 跟分批 source 的合成有五種策略、各自機會成本不同&lt;/strong>。沒有絕對最佳 — 選哪個取決於三個變數：&lt;/p>
&lt;ol>
&lt;li>Source 是否支援 server-side filter（capabilities）&lt;/li>
&lt;li>Match 密度（稀疏 vs 密集）&lt;/li>
&lt;li>UX 容忍度（要不要誠實顯示「掃描範圍」）&lt;/li>
&lt;/ol>
&lt;p>本文是 #55 &lt;a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位&lt;/a> 的解法展開、列出五個合理選項與適用情境。&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>對 source 的需求&lt;/th>
 &lt;th>對 UX 的影響&lt;/th>
 &lt;th>工程量&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>A&lt;/td>
 &lt;td>把 filter 推進 source 的 query&lt;/td>
 &lt;td>必須支援該 filter 條件&lt;/td>
 &lt;td>透明（無感）&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B&lt;/td>
 &lt;td>自動續抓直到湊滿 N 個 match&lt;/td>
 &lt;td>任何分批 source&lt;/td>
 &lt;td>透明（稍慢）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C&lt;/td>
 &lt;td>預先建獨立 index（每種 mode 一份）&lt;/td>
 &lt;td>能控 source 的 build pipeline&lt;/td>
 &lt;td>透明（最快）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D&lt;/td>
 &lt;td>誠實 UX 顯示「已掃 N / 命中 K」&lt;/td>
 &lt;td>任何 source&lt;/td>
 &lt;td>顯眼（多按鈕）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E&lt;/td>
 &lt;td>接受「filter 範圍 = 已載入」、不承諾 source 全集&lt;/td>
 &lt;td>任何 source&lt;/td>
 &lt;td>隱性語意縮小&lt;/td>
 &lt;td>最低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="五策略一句話總覽">五策略一句話總覽&lt;/h2>
&lt;p>每個策略各自一張獨立 pattern 卡片、本卡只給總覽與選擇規則。&lt;/p>
&lt;h3 id="策略-a推進-query">策略 A：推進 query&lt;/h3>
&lt;p>把 filter 條件變成 source 的 query 參數、source 端就回符合的。最優、無層錯位 — 但要 source 支援。詳見 &lt;a href="../pattern-query-side-pushdown/">#61 Pattern：推進 query&lt;/a>。&lt;/p>
&lt;h3 id="策略-b自動續抓直到湊滿">策略 B：自動續抓直到湊滿&lt;/h3>
&lt;p>抓一批 → filter → 不夠再抓 → 湊滿 N 個或 source 結束。需要上限保護避免拉爆。詳見 &lt;a href="../pattern-fetch-until-quota/">#60 Pattern：自動續抓&lt;/a>。&lt;/p>
&lt;h3 id="策略-c預先建獨立-index">策略 C：預先建獨立 index&lt;/h3>
&lt;p>Build time 為每種 filter mode 各建一份 source、runtime 切 mode = 切 source。前提是能控 build、mode 有限。詳見 &lt;a href="../pattern-multiple-indexes/">#65 Pattern：多 index&lt;/a>。&lt;/p>
&lt;h3 id="策略-d誠實進度-ux">策略 D：誠實進度 UX&lt;/h3>
&lt;p>保留 view 層 filter、UI 顯示「已掃 N / 命中 K / 共 M」三數字 + 「再掃一批」、使用者手動觸發續抓。詳見 &lt;a href="../pattern-honest-progress-ui/">#62 Pattern：誠實進度 UX&lt;/a>。&lt;/p>
&lt;h3 id="策略-e明示語意縮小">策略 E：明示語意縮小&lt;/h3>
&lt;p>明示告訴使用者「filter 範圍 = 已載入、不承諾全集」、不假裝是全集 filter。比 D 顯眼度低、但成本最低。詳見 &lt;a href="../pattern-explicit-semantic-narrowing/">#66 Pattern：明示語意縮小&lt;/a>。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>D 跟 E 都是 subset 上做、差別&lt;/strong>：D 用三數字持續顯示掃描範圍、E 用文字一次性告知。silent 縮小（既不三數字、也不告知）= 反模式、撞回 &lt;a href="../view-layer-filter-vs-source-layer/">#55 層錯位&lt;/a>。&lt;/p>&lt;/blockquote>
&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>Source 支援 server-side filter&lt;/td>
 &lt;td>A（最優）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Source 不支援、match 密度高、自動較好&lt;/td>
 &lt;td>B&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Source 不支援、能控 build、mode 有限&lt;/td>
 &lt;td>C&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Source 不支援、稀疏、要避免拉爆&lt;/td>
 &lt;td>D&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>原型期、不解決完美&lt;/td>
 &lt;td>E（明示語意縮小）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Source 一次性給完、無分批&lt;/td>
 &lt;td>view 層 filter 直接寫&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="多策略並用">多策略並用&lt;/h2>
&lt;p>實務上常見組合：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Filter 跟分批 source 的合成有五種策略、各自機會成本不同</strong>。沒有絕對最佳 — 選哪個取決於三個變數：</p>
<ol>
<li>Source 是否支援 server-side filter（capabilities）</li>
<li>Match 密度（稀疏 vs 密集）</li>
<li>UX 容忍度（要不要誠實顯示「掃描範圍」）</li>
</ol>
<p>本文是 #55 <a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位</a> 的解法展開、列出五個合理選項與適用情境。</p>
<hr>
<h2 id="五策略對照表">五策略對照表</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>一句話</th>
          <th>對 source 的需求</th>
          <th>對 UX 的影響</th>
          <th>工程量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A</td>
          <td>把 filter 推進 source 的 query</td>
          <td>必須支援該 filter 條件</td>
          <td>透明（無感）</td>
          <td>中-高</td>
      </tr>
      <tr>
          <td>B</td>
          <td>自動續抓直到湊滿 N 個 match</td>
          <td>任何分批 source</td>
          <td>透明（稍慢）</td>
          <td>中</td>
      </tr>
      <tr>
          <td>C</td>
          <td>預先建獨立 index（每種 mode 一份）</td>
          <td>能控 source 的 build pipeline</td>
          <td>透明（最快）</td>
          <td>高</td>
      </tr>
      <tr>
          <td>D</td>
          <td>誠實 UX 顯示「已掃 N / 命中 K」</td>
          <td>任何 source</td>
          <td>顯眼（多按鈕）</td>
          <td>低</td>
      </tr>
      <tr>
          <td>E</td>
          <td>接受「filter 範圍 = 已載入」、不承諾 source 全集</td>
          <td>任何 source</td>
          <td>隱性語意縮小</td>
          <td>最低</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="五策略一句話總覽">五策略一句話總覽</h2>
<p>每個策略各自一張獨立 pattern 卡片、本卡只給總覽與選擇規則。</p>
<h3 id="策略-a推進-query">策略 A：推進 query</h3>
<p>把 filter 條件變成 source 的 query 參數、source 端就回符合的。最優、無層錯位 — 但要 source 支援。詳見 <a href="../pattern-query-side-pushdown/">#61 Pattern：推進 query</a>。</p>
<h3 id="策略-b自動續抓直到湊滿">策略 B：自動續抓直到湊滿</h3>
<p>抓一批 → filter → 不夠再抓 → 湊滿 N 個或 source 結束。需要上限保護避免拉爆。詳見 <a href="../pattern-fetch-until-quota/">#60 Pattern：自動續抓</a>。</p>
<h3 id="策略-c預先建獨立-index">策略 C：預先建獨立 index</h3>
<p>Build time 為每種 filter mode 各建一份 source、runtime 切 mode = 切 source。前提是能控 build、mode 有限。詳見 <a href="../pattern-multiple-indexes/">#65 Pattern：多 index</a>。</p>
<h3 id="策略-d誠實進度-ux">策略 D：誠實進度 UX</h3>
<p>保留 view 層 filter、UI 顯示「已掃 N / 命中 K / 共 M」三數字 + 「再掃一批」、使用者手動觸發續抓。詳見 <a href="../pattern-honest-progress-ui/">#62 Pattern：誠實進度 UX</a>。</p>
<h3 id="策略-e明示語意縮小">策略 E：明示語意縮小</h3>
<p>明示告訴使用者「filter 範圍 = 已載入、不承諾全集」、不假裝是全集 filter。比 D 顯眼度低、但成本最低。詳見 <a href="../pattern-explicit-semantic-narrowing/">#66 Pattern：明示語意縮小</a>。</p>
<blockquote>
<p><strong>D 跟 E 都是 subset 上做、差別</strong>：D 用三數字持續顯示掃描範圍、E 用文字一次性告知。silent 縮小（既不三數字、也不告知）= 反模式、撞回 <a href="../view-layer-filter-vs-source-layer/">#55 層錯位</a>。</p></blockquote>
<hr>
<h2 id="選擇規則決定矩陣">選擇規則：決定矩陣</h2>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>建議策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 支援 server-side filter</td>
          <td>A（最優）</td>
      </tr>
      <tr>
          <td>Source 不支援、match 密度高、自動較好</td>
          <td>B</td>
      </tr>
      <tr>
          <td>Source 不支援、能控 build、mode 有限</td>
          <td>C</td>
      </tr>
      <tr>
          <td>Source 不支援、稀疏、要避免拉爆</td>
          <td>D</td>
      </tr>
      <tr>
          <td>原型期、不解決完美</td>
          <td>E（明示語意縮小）</td>
      </tr>
      <tr>
          <td>Source 一次性給完、無分批</td>
          <td>view 層 filter 直接寫</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="多策略並用">多策略並用</h2>
<p>實務上常見組合：</p>
<ul>
<li><strong>A + D fallback</strong>：query 推進失敗（如使用者用 source 不支援的條件）→ fallback 到 D</li>
<li><strong>B + 上限 → D</strong>：自動續抓到上限後切 D（顯示「已掃 N 筆、再掃？」）</li>
<li><strong>C + B 補強</strong>：預先 index 解一般 case、B 解 index 沒覆蓋的組合</li>
</ul>
<p>並用通常比單選有效、但複雜度也最高。詳細的疊加判準（解不同層 / 沒副作用衝突 / 增量成本可接受）見 <a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強策略</a> — 本表的「並用」就是 #75 的具體展現。</p>
<p>「先 ship 哪個策略、哪個下輪」見 <a href="../incremental-shipping-criteria/">#76 分批 ship 準則</a> — 例如 D（UX）通常先 ship、A/C（結構）下輪。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該選的策略起點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 是 SQL / ES / pagefind 且 filter 條件已索引</td>
          <td>A</td>
      </tr>
      <tr>
          <td>Source 是 pagefind 且 filter 是「title vs content」</td>
          <td>C（重 index 兩份）</td>
      </tr>
      <tr>
          <td>Source 不支援、預期 match 密集、要無感</td>
          <td>B</td>
      </tr>
      <tr>
          <td>工程量限制、能接受顯眼 UX</td>
          <td>D</td>
      </tr>
      <tr>
          <td>原型 / MVP、能接受語意縮小但要明示</td>
          <td>E（含語意聲明）</td>
      </tr>
      <tr>
          <td>使用者意圖明確要「全部命中」、source 不支援、match 稀疏</td>
          <td>A 或 C 重設計、不要 B（會拉爆）</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Filter × Source 沒有最佳解、只有「對齊三變數（capabilities / 密度 / UX）的取捨」。識別三變數、選對策略 → 比寫漂亮的程式重要。</p>
<p>跟 <a href="../external-component-collaboration-layers/">#45 跟外部組件合作的四層次</a> 同構：A 推進 query ≈ 公共介面層（最穩定）、C 多 index ≈ 邊界層（build pipeline 控制）、B 自動續抓 ≈ 邊界 DOM 層（client 補足）、D / E 誠實或縮小 ≈ 內部結構層（接受限制）。兩個原則的選擇順序都是「離 source 公共介面越近、合作越穩」。</p>
]]></content:encoded></item><item><title>Pattern：自動續抓直到湊滿 quota</title><link>https://tarrragon.github.io/blog/report/pattern-fetch-until-quota/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-fetch-until-quota/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>抓一批 → filter → 不夠就再抓 → 湊滿 N 個 match 或 source 結束。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 B。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援 server-side filter（不能用策略 A）&lt;/li>
&lt;li>不能控 build pipeline 重 index（不能用策略 C）&lt;/li>
&lt;li>Match 密度可預期、不會稀疏到要拉光整個 dataset&lt;/li>
&lt;li>使用者期望「filter 後自動湊夠 N 個」、不要手動續抓&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Source 支援 server-side filter（直接用策略 A）&lt;/li>
&lt;li>Match 稀疏、可能拉光整個 dataset 才湊到 N（換 D 誠實 UX）&lt;/li>
&lt;li>Source cardinality 大（10 萬筆）、不能拉太多次&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="必要元件">必要元件&lt;/h2>
&lt;h3 id="元件-1quota-跟上限">元件 1：Quota 跟上限&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">TARGET&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 期望湊滿的 match 數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">MAX_BATCHES&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 最多續抓次數（保護）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">MAX_TIME_MS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">5000&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 最大時間（保護）
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>沒有上限 = 稀疏時拉爆&lt;/strong>。兩個上限缺一不可。&lt;/p>
&lt;h3 id="元件-2loop-with-break-conditions">元件 2：Loop with break conditions&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">target&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">TARGET&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">collected&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">start&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&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="kd">let&lt;/span> &lt;span class="nx">batchCount&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">target&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">hasMore&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">batchCount&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">MAX_BATCHES&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">start&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">MAX_TIME_MS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(...&lt;/span>&lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">batchCount&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="nx">reachedQuota&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="nx">exhaustedSource&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">hasMore&lt;/span>&lt;span class="p">(),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="nx">hitLimit&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">batchCount&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">MAX_BATCHES&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="nx">start&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">MAX_TIME_MS&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>返回值含三個 flag、UI 用來判斷該顯示哪個狀態（湊滿 / 抓完無更多 / 撞到上限）。&lt;/p>
&lt;h3 id="元件-3可中斷">元件 3：可中斷&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">signal&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"> 2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">while&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="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">signal&lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">aborted&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">throw&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">DOMException&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;aborted&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;AbortError&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">signal&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// 使用
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">ctrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">AbortController&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">ctrl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">abort&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">ctrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">AbortController&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ctrl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">signal&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用者改 query / filter 時能立刻取消舊的續抓。&lt;strong>沒有可中斷 = 競態 bug&lt;/strong>（舊 query 的結果晚到、覆蓋新 query 的）。&lt;/p>
&lt;hr>
&lt;h2 id="ux-配套">UX 配套&lt;/h2>
&lt;h3 id="載入中顯示進度">載入中顯示進度&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;loading&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆 / 已命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3 / 10&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不顯示進度 = 使用者不知道是在等還是卡住。&lt;/p>
&lt;h3 id="結束時顯示原因">結束時顯示原因&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>結束原因&lt;/th>
 &lt;th>顯示&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>reachedQuota&lt;/code>&lt;/td>
 &lt;td>「找到 10 個結果」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>exhaustedSource&lt;/code>&lt;/td>
 &lt;td>「全部掃完、共找到 K 個」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>hitLimit&lt;/code>&lt;/td>
 &lt;td>「已掃 N 筆、找到 K 個。要繼續找嗎？」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不區分原因 = 使用者不知道為什麼停（同 #57 三狀態問題）。&lt;/p>
&lt;hr>
&lt;h2 id="反例">反例&lt;/h2>
&lt;h3 id="反例-1沒上限">反例 1：沒上限&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">target&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">hasMore&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">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">collected&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(...(&lt;/span>&lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">()).&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&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="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="c1">// 稀疏 match → 拉光整個 source
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="反例-2沒-abort-signal">反例 2：沒 abort signal&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">input&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;input&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 舊 query 的結果可能覆蓋新 query
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="反例-3每批序列化等">反例 3：每批序列化等&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">let&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="nx">MAX&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&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">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 序列、慢
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果 source 支援平行 fetch（多個 page 同時抓） → 改成平行更快：&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>抓一批 → filter → 不夠就再抓 → 湊滿 N 個 match 或 source 結束。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 B。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 不支援 server-side filter（不能用策略 A）</li>
<li>不能控 build pipeline 重 index（不能用策略 C）</li>
<li>Match 密度可預期、不會稀疏到要拉光整個 dataset</li>
<li>使用者期望「filter 後自動湊夠 N 個」、不要手動續抓</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Source 支援 server-side filter（直接用策略 A）</li>
<li>Match 稀疏、可能拉光整個 dataset 才湊到 N（換 D 誠實 UX）</li>
<li>Source cardinality 大（10 萬筆）、不能拉太多次</li>
</ul>
<hr>
<h2 id="必要元件">必要元件</h2>
<h3 id="元件-1quota-跟上限">元件 1：Quota 跟上限</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="kr">const</span> <span class="nx">TARGET</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>       <span class="c1">// 期望湊滿的 match 數
</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">MAX_BATCHES</span> <span class="o">=</span> <span class="mi">20</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="kr">const</span> <span class="nx">MAX_TIME_MS</span> <span class="o">=</span> <span class="mi">5000</span><span class="p">;</span> <span class="c1">// 最大時間（保護）
</span></span></span></code></pre></div><p><strong>沒有上限 = 稀疏時拉爆</strong>。兩個上限缺一不可。</p>
<h3 id="元件-2loop-with-break-conditions">元件 2：Loop with break conditions</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="kr">async</span> <span class="kd">function</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">,</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">TARGET</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">collected</span> <span class="o">=</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">start</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">let</span> <span class="nx">batchCount</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">while</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">collected</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="nx">target</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">hasMore</span><span class="p">()</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">batchCount</span> <span class="o">&lt;</span> <span class="nx">MAX_BATCHES</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">start</span> <span class="o">&lt;</span> <span class="nx">MAX_TIME_MS</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nx">collected</span><span class="p">.</span><span class="nx">push</span><span class="p">(...</span><span class="nx">batch</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">matches</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">batchCount</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">collected</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nx">reachedQuota</span><span class="o">:</span> <span class="nx">collected</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;=</span> <span class="nx">target</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">exhaustedSource</span><span class="o">:</span> <span class="o">!</span><span class="nx">hasMore</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nx">hitLimit</span><span class="o">:</span> <span class="nx">batchCount</span> <span class="o">&gt;=</span> <span class="nx">MAX_BATCHES</span> <span class="o">||</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">start</span> <span class="o">&gt;=</span> <span class="nx">MAX_TIME_MS</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>返回值含三個 flag、UI 用來判斷該顯示哪個狀態（湊滿 / 抓完無更多 / 撞到上限）。</p>
<h3 id="元件-3可中斷">元件 3：可中斷</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="kr">async</span> <span class="kd">function</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">,</span> <span class="nx">target</span><span class="p">,</span> <span class="nx">signal</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="k">while</span> <span class="p">(...)</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">signal</span><span class="o">?</span><span class="p">.</span><span class="nx">aborted</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nx">DOMException</span><span class="p">(</span><span class="s1">&#39;aborted&#39;</span><span class="p">,</span> <span class="s1">&#39;AbortError&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">({</span> <span class="nx">signal</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></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">// 使用
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">ctrl</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#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">13</span><span class="cl">  <span class="nx">ctrl</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="nx">ctrl</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="nx">ctrl</span><span class="p">.</span><span class="nx">signal</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>使用者改 query / filter 時能立刻取消舊的續抓。<strong>沒有可中斷 = 競態 bug</strong>（舊 query 的結果晚到、覆蓋新 query 的）。</p>
<hr>
<h2 id="ux-配套">UX 配套</h2>
<h3 id="載入中顯示進度">載入中顯示進度</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;loading&#34;</span><span class="p">&gt;</span>已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆 / 已命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3 / 10<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p>不顯示進度 = 使用者不知道是在等還是卡住。</p>
<h3 id="結束時顯示原因">結束時顯示原因</h3>
<table>
  <thead>
      <tr>
          <th>結束原因</th>
          <th>顯示</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>reachedQuota</code></td>
          <td>「找到 10 個結果」</td>
      </tr>
      <tr>
          <td><code>exhaustedSource</code></td>
          <td>「全部掃完、共找到 K 個」</td>
      </tr>
      <tr>
          <td><code>hitLimit</code></td>
          <td>「已掃 N 筆、找到 K 個。要繼續找嗎？」</td>
      </tr>
  </tbody>
</table>
<p>不區分原因 = 使用者不知道為什麼停（同 #57 三狀態問題）。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1沒上限">反例 1：沒上限</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="k">while</span> <span class="p">(</span><span class="nx">collected</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="nx">target</span> <span class="o">&amp;&amp;</span> <span class="nx">hasMore</span><span class="p">())</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">collected</span><span class="p">.</span><span class="nx">push</span><span class="p">(...(</span><span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">()).</span><span class="nx">filter</span><span class="p">(</span><span class="nx">matches</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 稀疏 match → 拉光整個 source
</span></span></span></code></pre></div><h3 id="反例-2沒-abort-signal">反例 2：沒 abort signal</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="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="kr">async</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">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="nx">matches</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">render</span><span class="p">(</span><span class="nx">r</span><span class="p">);</span>  <span class="c1">// 舊 query 的結果可能覆蓋新 query
</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><h3 id="反例-3每批序列化等">反例 3：每批序列化等</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="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">MAX</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</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">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</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="c1">// ...
</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>如果 source 支援平行 fetch（多個 page 同時抓） → 改成平行更快：</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="kr">const</span> <span class="nx">batches</span> <span class="o">=</span> <span class="kr">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span><span class="nx">fetch</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="nx">fetch</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="nx">fetch</span><span class="p">(</span><span class="mi">2</span><span class="p">)]);</span></span></span></code></pre></div><p>但平行有 over-fetch 風險（湊滿後其他批白抓） — 適合 match 密度高的情境。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<ul>
<li>跟 #61 <a href="../pattern-query-side-pushdown/">Pattern：推進 query</a>（待補）：A 是最優、B 是 source 不支援時的退路</li>
<li>跟 #62 <a href="../pattern-honest-progress-ui/">Pattern：誠實進度 UX</a>（待補）：B 撞到上限後 fallback 到誠實 UX</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 不支援 filter、要湊滿 N 個結果</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>寫了 while loop 但沒上限</td>
          <td>補 MAX_BATCHES + MAX_TIME_MS</td>
      </tr>
      <tr>
          <td>Input 改變時舊的續抓還在跑</td>
          <td>補 AbortController</td>
      </tr>
      <tr>
          <td>結束時不知道是「湊滿」「掃完」「撞上限」</td>
          <td>補三個 flag、UI 分支顯示</td>
      </tr>
      <tr>
          <td>Match 稀疏、續抓 50 次才湊到 1 個</td>
          <td>換策略 — B 不適合稀疏 case</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：自動續抓的價值在「使用者透明」、但成本是「上限保護必要」。沒上限的 B 比 silent post-filter 更糟（會拉爆）。</p>
]]></content:encoded></item><item><title>Pattern：把 filter 推進 query 引擎</title><link>https://tarrragon.github.io/blog/report/pattern-query-side-pushdown/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-query-side-pushdown/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>把 filter 變成 source 的 query 參數、source 端就回符合的、client 不 post-filter。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 A。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 支援該 filter 條件（已索引、能在 query 表達）&lt;/li>
&lt;li>想避免任何 client-side post-filter&lt;/li>
&lt;li>想避免層錯位（見 #55）&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援（pagefind 對 title-only 沒 native 支援）&lt;/li>
&lt;li>條件需要 client-side 計算（依 viewport / 隨機抽樣）&lt;/li>
&lt;li>推進 query 後 cardinality 仍大、還是要 paginate（這時 A + B 並用）&lt;/li>
&lt;/ul>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Query 參數&lt;/td>
 &lt;td>&lt;code>?type=post&amp;amp;tag=js&lt;/code>&lt;/td>
 &lt;td>最低、改 URL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter API&lt;/td>
 &lt;td>&lt;code>pagefind.search(q, { filters: { type: 'post' } })&lt;/code>&lt;/td>
 &lt;td>低、用 SDK&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Re-query&lt;/td>
 &lt;td>重新呼叫 search、不是同個 result 集再過濾&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index 重建&lt;/td>
 &lt;td>Build 時加新欄位 / 新 index&lt;/td>
 &lt;td>中-高、要 build&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema 修改&lt;/td>
 &lt;td>改 DB schema、加欄位、reindex&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選哪一層 = source 的 capabilities 決定。&lt;/p>
&lt;hr>
&lt;h2 id="評估-source-capabilities">評估 Source Capabilities&lt;/h2>
&lt;p>寫之前讀 source docs / API spec、列出：&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>Source 接受哪些 filter 條件？&lt;/td>
 &lt;td>&lt;code>=&lt;/code>, &lt;code>IN&lt;/code>, &lt;code>BETWEEN&lt;/code>, full-text, &amp;hellip;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>哪些欄位已索引？&lt;/td>
 &lt;td>&lt;code>type&lt;/code>, &lt;code>tag&lt;/code>, &lt;code>date&lt;/code> (not &lt;code>title&lt;/code>)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>哪些 filter 不支援、需要重 index？&lt;/td>
 &lt;td>&lt;code>title contains&lt;/code>（需 full-text title）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Filter 有沒有 cost cap（rate limit）？&lt;/td>
 &lt;td>100 query / sec&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不評估就寫 = 寫到一半發現 source 不支援、回頭走策略 B 或 C。&lt;/p>
&lt;hr>
&lt;h2 id="範例pagefind">範例：Pagefind&lt;/h2>
&lt;h3 id="支援的-filter">支援的 filter&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// pagefind 已支援 filter（透過 _pagefind/filter.json）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">pagefind&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;keyword&amp;#39;&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="nx">filters&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;post&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 支援
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">tag&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">any&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;js&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;css&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="p">},&lt;/span> &lt;span class="c1">// 支援多選
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="不支援的-filter">不支援的 filter&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// pagefind 不支援「只搜 title」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">// 因為 pagefind 的 search 對 full-text、不分區
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">pagefind&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;keyword&amp;#39;&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="nx">scope&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;title-only&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 不存在（不支援）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>要解決：&lt;/p>
&lt;ul>
&lt;li>方案 1：build 時用 &lt;code>data-pagefind-body&lt;/code> 把 title 標成獨立 region、用 &lt;code>body&lt;/code> filter（pagefind v1.1+）&lt;/li>
&lt;li>方案 2：建兩個獨立 index（一個只 index title、一個只 index content） — 走策略 C&lt;/li>
&lt;li>方案 3：放棄推進 query、用策略 B 自動續抓 + post-filter&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="跟原本-query-邏輯的並用">跟原本 query 邏輯的並用&lt;/h2>
&lt;p>推進 filter 通常不取代原本 query、是「補上條件」：&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>把 filter 變成 source 的 query 參數、source 端就回符合的、client 不 post-filter。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 A。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 支援該 filter 條件（已索引、能在 query 表達）</li>
<li>想避免任何 client-side post-filter</li>
<li>想避免層錯位（見 #55）</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Source 不支援（pagefind 對 title-only 沒 native 支援）</li>
<li>條件需要 client-side 計算（依 viewport / 隨機抽樣）</li>
<li>推進 query 後 cardinality 仍大、還是要 paginate（這時 A + B 並用）</li>
</ul>
<hr>
<h2 id="推進的層次">推進的層次</h2>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>範例</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 參數</td>
          <td><code>?type=post&amp;tag=js</code></td>
          <td>最低、改 URL</td>
      </tr>
      <tr>
          <td>Filter API</td>
          <td><code>pagefind.search(q, { filters: { type: 'post' } })</code></td>
          <td>低、用 SDK</td>
      </tr>
      <tr>
          <td>Re-query</td>
          <td>重新呼叫 search、不是同個 result 集再過濾</td>
          <td>低</td>
      </tr>
      <tr>
          <td>Index 重建</td>
          <td>Build 時加新欄位 / 新 index</td>
          <td>中-高、要 build</td>
      </tr>
      <tr>
          <td>Schema 修改</td>
          <td>改 DB schema、加欄位、reindex</td>
          <td>高</td>
      </tr>
  </tbody>
</table>
<p>選哪一層 = source 的 capabilities 決定。</p>
<hr>
<h2 id="評估-source-capabilities">評估 Source Capabilities</h2>
<p>寫之前讀 source docs / API spec、列出：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>答案範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 接受哪些 filter 條件？</td>
          <td><code>=</code>, <code>IN</code>, <code>BETWEEN</code>, full-text, &hellip;</td>
      </tr>
      <tr>
          <td>哪些欄位已索引？</td>
          <td><code>type</code>, <code>tag</code>, <code>date</code> (not <code>title</code>)</td>
      </tr>
      <tr>
          <td>哪些 filter 不支援、需要重 index？</td>
          <td><code>title contains</code>（需 full-text title）</td>
      </tr>
      <tr>
          <td>Filter 有沒有 cost cap（rate limit）？</td>
          <td>100 query / sec</td>
      </tr>
  </tbody>
</table>
<p>不評估就寫 = 寫到一半發現 source 不支援、回頭走策略 B 或 C。</p>
<hr>
<h2 id="範例pagefind">範例：Pagefind</h2>
<h3 id="支援的-filter">支援的 filter</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="c1">// pagefind 已支援 filter（透過 _pagefind/filter.json）
</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">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="s1">&#39;keyword&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;post&#39;</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="nx">tag</span><span class="o">:</span> <span class="p">{</span> <span class="nx">any</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;js&#39;</span><span class="p">,</span> <span class="s1">&#39;css&#39;</span><span class="p">]</span> <span class="p">},</span> <span class="c1">// 支援多選
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="p">},</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h3 id="不支援的-filter">不支援的 filter</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="c1">// pagefind 不支援「只搜 title」
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 因為 pagefind 的 search 對 full-text、不分區
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="s1">&#39;keyword&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">scope</span><span class="o">:</span> <span class="s1">&#39;title-only&#39;</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="p">});</span></span></span></code></pre></div><p>要解決：</p>
<ul>
<li>方案 1：build 時用 <code>data-pagefind-body</code> 把 title 標成獨立 region、用 <code>body</code> filter（pagefind v1.1+）</li>
<li>方案 2：建兩個獨立 index（一個只 index title、一個只 index content） — 走策略 C</li>
<li>方案 3：放棄推進 query、用策略 B 自動續抓 + post-filter</li>
</ul>
<hr>
<h2 id="跟原本-query-邏輯的並用">跟原本 query 邏輯的並用</h2>
<p>推進 filter 通常不取代原本 query、是「補上條件」：</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">// 使用者輸入 query &#34;css&#34;、選 type=post
</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">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="s1">&#39;css&#39;</span><span class="p">,</span> <span class="p">{</span>  <span class="c1">// query
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;post&#39;</span> <span class="p">},</span>              <span class="c1">// filter
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 兩個都進 source、source 算交集
</span></span></span></code></pre></div><p>Filter 跟 query 是不同維度：query 是「找什麼」、filter 是「在哪些範圍找」。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1推進不完全留-client-side-post-filter-補">反例 1：推進不完全、留 client-side post-filter 補</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="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">q</span><span class="p">,</span> <span class="p">{</span> <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span> <span class="nx">type</span><span class="o">:</span> <span class="s1">&#39;post&#39;</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">filtered</span> <span class="o">=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">results</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">x</span> <span class="p">=&gt;</span> <span class="nx">x</span><span class="p">.</span><span class="nx">title</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">q</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// ↑ 這行還是 #55 層錯位
</span></span></span></code></pre></div><p>如果 source 不支援 title-filter、不要用「半推進」 — 直接走策略 C 或 B。</p>
<h3 id="反例-2忽略-cost-cap">反例 2：忽略 cost cap</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="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">,</span> <span class="kr">async</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="c1">// 每個鍵盤事件 fire 一個 search query
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span> <span class="p">{</span> <span class="nx">filters</span><span class="o">:</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></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// → query rate 100+/秒、撞 rate limit
</span></span></span></code></pre></div><p>加 debounce：</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="kd">let</span> <span class="nx">timer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">input</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;input&#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">3</span><span class="cl">  <span class="nx">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">timer</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span> <span class="p">...),</span> <span class="mi">200</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h3 id="反例-3客製欄位沒進-index寫了-query-失效">反例 3：客製欄位沒進 index、寫了 query 失效</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="c1">// 期望 filter 「閱讀時間 &gt; 5 分鐘」
</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">r</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">pagefind</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">q</span><span class="p">,</span> <span class="p">{</span> <span class="nx">filters</span><span class="o">:</span> <span class="p">{</span> <span class="nx">readingTime</span><span class="o">:</span> <span class="p">{</span> <span class="nx">gt</span><span class="o">:</span> <span class="mi">5</span> <span class="p">}</span> <span class="p">}</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// → 但 build 時沒把 readingTime 進 filter index → filter 被忽略
</span></span></span></code></pre></div><p>預期 source 不支援 → 評估「是否值得加進 index」（成本 vs 使用率）。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<ul>
<li>A 是最優 — 在 source capabilities 範圍內優先選</li>
<li>A 不可行 → 評估 C（建獨立 index）</li>
<li>C 也不可行 → 退到 B（自動續抓）</li>
<li>都不可行 → D（誠實 UX）</li>
</ul>
<p><strong>選擇順序：A → C → B → D</strong>。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter 條件能在 source 端表達</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>Source 不支援、考慮要不要重 index</td>
          <td>評估 C 的成本</td>
      </tr>
      <tr>
          <td>用了 filter 還寫 client-side post-filter</td>
          <td>半推進是反模式、要嘛全推進、要嘛換策略</td>
      </tr>
      <tr>
          <td>Filter 觸發 query rate 高</td>
          <td>加 debounce / throttle</td>
      </tr>
      <tr>
          <td>Query 跟 filter 概念混淆</td>
          <td>區分：query = 「找什麼」、filter = 「範圍」</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：能推進 query 就推 — 沒層錯位、沒 silent 失敗、跟使用者意圖最近。但前提是 source 支援；不支援就要退到 B / C / D、不要做半推進。</p>
]]></content:encoded></item><item><title>Pattern：誠實進度 UX（已掃 N / 命中 K / 共 M）</title><link>https://tarrragon.github.io/blog/report/pattern-honest-progress-ui/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-honest-progress-ui/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>當 filter 必然有層錯位、用「已掃 N / 命中 K / 共 M」三數字 + 「再掃一批」按鈕讓使用者看見掃描範圍、自己決定要不要續抓。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 D。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援 server-side filter（A 不可行）&lt;/li>
&lt;li>不能或不值得重 index（C 不可行）&lt;/li>
&lt;li>Match 稀疏或不可預期、自動續抓（B）會拉爆&lt;/li>
&lt;li>工程量限制、原型期 / MVP&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Filter 是主要互動模式（使用者預期「自動全找完」）&lt;/li>
&lt;li>三數字會讓 UI 太複雜&lt;/li>
&lt;li>使用者完全不在意「掃描範圍」&lt;/li>
&lt;/ul>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>已掃 N&lt;/td>
 &lt;td>已從 source 載入並 filter 過的筆數&lt;/td>
 &lt;td>client 累計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>命中 K&lt;/td>
 &lt;td>已掃 N 筆中、符合 filter 的筆數&lt;/td>
 &lt;td>client 累計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>共 M&lt;/td>
 &lt;td>Source 總筆數（如果 source 知道）&lt;/td>
 &lt;td>source meta（可選）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最少要顯示「已掃 N / 命中 K」 — 沒有 N 使用者不知道掃描範圍、沒有 K 使用者不知道有沒有命中。&lt;/p>
&lt;p>「共 M」可選 — 有的 source（pagefind）會給 total count、有的（streaming）不會。&lt;/p>
&lt;hr>
&lt;h2 id="ui-模板">UI 模板&lt;/h2>
&lt;h3 id="基本版">基本版&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆 / 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="含總數">含總數&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> / &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>~150&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆 — 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="含結束狀態呼應-57-三狀態">含結束狀態（呼應 #57 三狀態）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Loading --&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>掃描中... 已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> / 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&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 class="c">&amp;lt;!-- Partial（還可續） --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> / 命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>3&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- End（掃完） --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已全部掃完、共命中 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>12&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 筆&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Empty (filter) --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-status&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>已掃 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>24&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">strong&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>、沒有命中
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>再掃一批&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">button&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> 或 &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>清除 filter&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">a&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="進度更新時機">進度更新時機&lt;/h2>
&lt;h3 id="即時更新每筆">即時更新（每筆）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kr">const&lt;/span> &lt;span class="nx">item&lt;/span> &lt;span class="k">of&lt;/span> &lt;span class="nx">stream&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">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">scanned&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&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="nx">matched&lt;/span>&lt;span class="o">++&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">appendResult&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nx">updateUI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scanned&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">matched&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 每筆更新
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>UX 順、但 DOM 操作頻繁、可能 jank。&lt;/p>
&lt;h3 id="批次更新每批">批次更新（每批）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">batch&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchNext&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">scanned&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&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="kr">const&lt;/span> &lt;span class="nx">m&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">batch&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nx">matched&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="nx">m&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nx">appendResults&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">m&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">updateUI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">scanned&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">matched&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 每批一次
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM 操作少、但 UX 不夠順（一段時間沒動）。&lt;/p>
&lt;h3 id="推薦每批--載入中-spinner">推薦：每批 + 載入中 spinner&lt;/h3>
&lt;p>批次後更新數字、批次間顯示 spinner。最平衡。&lt;/p>
&lt;hr>
&lt;h2 id="跟自動續抓b的混合">跟自動續抓（B）的混合&lt;/h2>
&lt;p>可以做成「初始自動續抓 N 批、之後切誠實 UX」：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">searchWithFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&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">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 初始自動續抓 3 批（湊一些結果）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">fetchUntilQuota&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">autoBatches&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">3&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>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 之後使用者手動點「再掃一批」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">showHonestProgressUI&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>混合的好處：使用者一進來就有結果（不是空畫面）、之後續抓由使用者決定。&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>當 filter 必然有層錯位、用「已掃 N / 命中 K / 共 M」三數字 + 「再掃一批」按鈕讓使用者看見掃描範圍、自己決定要不要續抓。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 D。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 不支援 server-side filter（A 不可行）</li>
<li>不能或不值得重 index（C 不可行）</li>
<li>Match 稀疏或不可預期、自動續抓（B）會拉爆</li>
<li>工程量限制、原型期 / MVP</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Filter 是主要互動模式（使用者預期「自動全找完」）</li>
<li>三數字會讓 UI 太複雜</li>
<li>使用者完全不在意「掃描範圍」</li>
</ul>
<hr>
<h2 id="三數字的語意">三數字的語意</h2>
<table>
  <thead>
      <tr>
          <th>數字</th>
          <th>意思</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已掃 N</td>
          <td>已從 source 載入並 filter 過的筆數</td>
          <td>client 累計</td>
      </tr>
      <tr>
          <td>命中 K</td>
          <td>已掃 N 筆中、符合 filter 的筆數</td>
          <td>client 累計</td>
      </tr>
      <tr>
          <td>共 M</td>
          <td>Source 總筆數（如果 source 知道）</td>
          <td>source meta（可選）</td>
      </tr>
  </tbody>
</table>
<p>最少要顯示「已掃 N / 命中 K」 — 沒有 N 使用者不知道掃描範圍、沒有 K 使用者不知道有沒有命中。</p>
<p>「共 M」可選 — 有的 source（pagefind）會給 total count、有的（streaming）不會。</p>
<hr>
<h2 id="ui-模板">UI 模板</h2>
<h3 id="基本版">基本版</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;filter-status&#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">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆 / 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</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="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">4</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><h3 id="含總數">含總數</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;filter-status&#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">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> / <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>~150<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> 筆 — 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</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="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">4</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><h3 id="含結束狀態呼應-57-三狀態">含結束狀態（呼應 #57 三狀態）</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;!-- Loading --&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;filter-status&#34;</span><span class="p">&gt;</span>掃描中... 已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> / 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</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></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c">&lt;!-- Partial（還可續） --&gt;</span>
</span></span><span class="line"><span class="ln"> 5</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;filter-status&#34;</span><span class="p">&gt;</span>已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> / 命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">strong</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><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="c">&lt;!-- End（掃完） --&gt;</span>
</span></span><span class="line"><span class="ln">10</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;filter-status&#34;</span><span class="p">&gt;</span>已全部掃完、共命中 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>12<span class="p">&lt;/</span><span class="nt">strong</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">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c">&lt;!-- Empty (filter) --&gt;</span>
</span></span><span class="line"><span class="ln">13</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;filter-status&#34;</span><span class="p">&gt;</span>已掃 <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>24<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span>、沒有命中
</span></span><span class="line"><span class="ln">14</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 class="p">&lt;</span><span class="nt">a</span><span class="p">&gt;</span>清除 filter<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><hr>
<h2 id="進度更新時機">進度更新時機</h2>
<h3 id="即時更新每筆">即時更新（每筆）</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="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">item</span> <span class="k">of</span> <span class="nx">stream</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">scanned</span><span class="o">++</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">matches</span><span class="p">(</span><span class="nx">item</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">matched</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">appendResult</span><span class="p">(</span><span class="nx">item</span><span class="p">);</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 class="nx">updateUI</span><span class="p">(</span><span class="nx">scanned</span><span class="p">,</span> <span class="nx">matched</span><span class="p">);</span>  <span class="c1">// 每筆更新
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>UX 順、但 DOM 操作頻繁、可能 jank。</p>
<h3 id="批次更新每批">批次更新（每批）</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="kr">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetchNext</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">scanned</span> <span class="o">+=</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">length</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">m</span> <span class="o">=</span> <span class="nx">batch</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">matches</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">matched</span> <span class="o">+=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">appendResults</span><span class="p">(</span><span class="nx">m</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">updateUI</span><span class="p">(</span><span class="nx">scanned</span><span class="p">,</span> <span class="nx">matched</span><span class="p">);</span>  <span class="c1">// 每批一次
</span></span></span></code></pre></div><p>DOM 操作少、但 UX 不夠順（一段時間沒動）。</p>
<h3 id="推薦每批--載入中-spinner">推薦：每批 + 載入中 spinner</h3>
<p>批次後更新數字、批次間顯示 spinner。最平衡。</p>
<hr>
<h2 id="跟自動續抓b的混合">跟自動續抓（B）的混合</h2>
<p>可以做成「初始自動續抓 N 批、之後切誠實 UX」：</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="kr">async</span> <span class="kd">function</span> <span class="nx">searchWithFilter</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 初始自動續抓 3 批（湊一些結果）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kr">await</span> <span class="nx">fetchUntilQuota</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="nx">autoBatches</span><span class="o">:</span> <span class="mi">3</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// 之後使用者手動點「再掃一批」
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">showHonestProgressUI</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>混合的好處：使用者一進來就有結果（不是空畫面）、之後續抓由使用者決定。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1只顯示命中-k不顯示已掃-n">反例 1：只顯示「命中 K」、不顯示「已掃 N」</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="p">&gt;</span>找到 3 筆結果<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p>使用者不知道是從多少筆裡找的、不知道「再掃會不會有」。</p>
<h3 id="反例-2只顯示共-m--n進度條沒分已掃命中">反例 2：只顯示「共 M / N」進度條、沒分「已掃」「命中」</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">progress</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;24&#34;</span> <span class="na">max</span><span class="o">=</span><span class="s">&#34;150&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">progress</span><span class="p">&gt;</span></span></span></code></pre></div><p>進度條告訴使用者「load 進度」、但「load 進度 ≠ filter 進度」。沒命中時使用者不知道為什麼進度走了 24% 但畫面沒結果。</p>
<h3 id="反例-3再掃一批沒做">反例 3：「再掃一批」沒做</h3>
<p>只顯示三數字、沒提供續抓 button — 使用者看到「已掃 24 沒命中」、不知道下一步。</p>
<hr>
<h2 id="跟-57-三狀態的關係">跟 #57 三狀態的關係</h2>
<p>誠實進度 UX 是 #57 <a href="../loading-empty-end-state-distinction/">Loading / Empty / End 三狀態的區分</a> 在「filter + 分批」情境下的具體實作。三數字提供區分三狀態的訊號：</p>
<table>
  <thead>
      <tr>
          <th>#57 狀態</th>
          <th>對應的三數字組合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loading</td>
          <td>已掃增加中、N 還在跑</td>
      </tr>
      <tr>
          <td>Empty (filter)</td>
          <td>已掃 = 24、命中 = 0、還有 → 「再掃」</td>
      </tr>
      <tr>
          <td>End</td>
          <td>已掃 = M、命中 = K（K 可能 0）</td>
      </tr>
      <tr>
          <td>Partial</td>
          <td>已掃 &lt; M、命中 ≥ 1、還有 → 「再掃」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter 後可能 0 筆、source 還有未載入</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>UI 上只有「找到 K 筆」、沒有「已掃 N」</td>
          <td>補 N — 否則使用者無法判斷</td>
      </tr>
      <tr>
          <td>沒有「再掃一批」按鈕</td>
          <td>補 — 給使用者下一步行動</td>
      </tr>
      <tr>
          <td>工程量允許做策略 A / C</td>
          <td>用 A / C、誠實 UX 是退路</td>
      </tr>
      <tr>
          <td>Match 密集、自動續抓不會爆</td>
          <td>用策略 B、誠實 UX 太顯眼</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：誠實 UX 不是「lazy 解法」、是「sourcing 限制下的合理透明度」。給使用者三數字 + 行動選項、比假裝完美但 silent 失敗好。</p>
<p>跟 <a href="../override-depth-cost-report/">#19 覆寫深度的成本告知</a> 同源：兩者都是「把實作的限制 / 代價攤給使用者、讓使用者參與決策」。差別在 #19 是「實作前告知工程成本」、本卡是「runtime 持續顯示掃描成本」 — 攤出來的位置不同、原則一致：silent 累積負擔是反模式。</p>
]]></content:encoded></item><item><title>資料源的形狀決定 feature 的形狀</title><link>https://tarrragon.github.io/blog/report/data-source-shape-defines-feature-shape/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/data-source-shape-defines-feature-shape/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Feature 的設計受資料源的形狀約束、不能憑 UI 想要的形狀去倒推&lt;/strong>。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料源形狀&lt;/th>
 &lt;th>對 feature 的硬約束&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一次性 fetch（靜態 / API 全集）&lt;/td>
 &lt;td>Filter / sort / count 都安全可在任意層做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>分批 fetch（pagination）&lt;/td>
 &lt;td>Filter / sort 必須跟 source 同層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Streaming（SSE / iterator）&lt;/td>
 &lt;td>結果可能無上限、count 是不確定值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cached + revalidate&lt;/td>
 &lt;td>兩個 dataset 並存、要決定哪個 winning&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>憑 UI 倒推資料層 =「我希望畫面這樣呈現、所以資料層應該這樣」 → 多半會在錯誤的層做錯誤的操作（見 #55 &lt;a href="../view-layer-filter-vs-source-layer/">層錯位&lt;/a>）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼會憑-ui-倒推">為什麼會憑 UI 倒推&lt;/h2>
&lt;h3 id="ui-設計通常先動">UI 設計通常先動&lt;/h3>
&lt;p>設計師畫 wireframe、PM 描述體驗、執行者看到的是「畫面該長什麼樣」 — 資料層的限制不在 wireframe 裡。&lt;/p>
&lt;h3 id="ui-形狀對資料層假設過強">UI 形狀對資料層假設過強&lt;/h3>
&lt;p>UI 上「filter 拉桿」這個元件、隱含假設「資料能立即過濾」 — 但如果資料是分批 fetch、立即過濾在資料層不成立。執行者按 UI 寫 → view 層 post-filter → 撞上層錯位。&lt;/p>
&lt;h3 id="能用訊號早於對齊資料形狀">「能用」訊號早於「對齊資料形狀」&lt;/h3>
&lt;p>寫完 view 層 filter、手動測一次能用、覺得對 — 但能用的範圍是「已載入子集」、不是「完整 dataset」。資料形狀的限制要刻意對照才看得到。&lt;/p>
&lt;hr>
&lt;h2 id="多面向資料源形狀的不同類型">多面向：資料源形狀的不同類型&lt;/h2>
&lt;h3 id="形狀-1一次性給完整-dataset">形狀 1：一次性給完整 dataset&lt;/h3>
&lt;p>範例：靜態 JSON、SSR 完整渲染、API 一次回全集（&amp;lt; 1MB）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Feature 設計&lt;/th>
 &lt;th>安全與否&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>任意層 filter&lt;/td>
 &lt;td>安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任意層 sort&lt;/td>
 &lt;td>安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Count&lt;/td>
 &lt;td>安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pagination&lt;/td>
 &lt;td>不需要&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這類 source 是「最寬容」的、UI 想怎麼設計都行。&lt;/p>
&lt;h3 id="形狀-2分批-fetchpagination">形狀 2：分批 fetch（pagination）&lt;/h3>
&lt;p>範例：pagefind、infinite scroll、cursor-based API。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Feature 設計&lt;/th>
 &lt;th>限制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Filter&lt;/td>
 &lt;td>必須跟 source 同層（A）或自動續抓（B）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sort&lt;/td>
 &lt;td>必須是 server-side sort、不能 client 重排&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Count&lt;/td>
 &lt;td>通常需要 source 提供 total（pagefind 有）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「跳到最後一頁」&lt;/td>
 &lt;td>需要 cursor / offset 支援&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>UI 設計時要避開：「立即 filter」「立即 sort」「Show all」 — 這些假設 dataset 已 materialize。&lt;/p>
&lt;h3 id="形狀-3streaming--async-iterator">形狀 3：Streaming / async iterator&lt;/h3>
&lt;p>範例：SSE、WebSocket push、async iterator from generator、log tail。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Feature 設計&lt;/th>
 &lt;th>限制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Filter&lt;/td>
 &lt;td>可在 stream 裡做（透明）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sort&lt;/td>
 &lt;td>不能 — stream 沒終點、無法 sort&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Count&lt;/td>
 &lt;td>「目前累計」、不是「總數」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>進度條&lt;/td>
 &lt;td>只能顯示「已收 N 筆」、不能 % progress&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>UI 設計時要避開：「sort by 任意欄位」「總共 X 筆」「進度條 50%」 — 這些假設有限終點。&lt;/p>
&lt;h3 id="形狀-4cached--revalidate">形狀 4：Cached + revalidate&lt;/h3>
&lt;p>範例：service worker cache、SWR、HTTP cache、IndexedDB cache。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Feature 的設計受資料源的形狀約束、不能憑 UI 想要的形狀去倒推</strong>。</p>
<table>
  <thead>
      <tr>
          <th>資料源形狀</th>
          <th>對 feature 的硬約束</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一次性 fetch（靜態 / API 全集）</td>
          <td>Filter / sort / count 都安全可在任意層做</td>
      </tr>
      <tr>
          <td>分批 fetch（pagination）</td>
          <td>Filter / sort 必須跟 source 同層</td>
      </tr>
      <tr>
          <td>Streaming（SSE / iterator）</td>
          <td>結果可能無上限、count 是不確定值</td>
      </tr>
      <tr>
          <td>Cached + revalidate</td>
          <td>兩個 dataset 並存、要決定哪個 winning</td>
      </tr>
  </tbody>
</table>
<p>憑 UI 倒推資料層 =「我希望畫面這樣呈現、所以資料層應該這樣」 → 多半會在錯誤的層做錯誤的操作（見 #55 <a href="../view-layer-filter-vs-source-layer/">層錯位</a>）。</p>
<hr>
<h2 id="為什麼會憑-ui-倒推">為什麼會憑 UI 倒推</h2>
<h3 id="ui-設計通常先動">UI 設計通常先動</h3>
<p>設計師畫 wireframe、PM 描述體驗、執行者看到的是「畫面該長什麼樣」 — 資料層的限制不在 wireframe 裡。</p>
<h3 id="ui-形狀對資料層假設過強">UI 形狀對資料層假設過強</h3>
<p>UI 上「filter 拉桿」這個元件、隱含假設「資料能立即過濾」 — 但如果資料是分批 fetch、立即過濾在資料層不成立。執行者按 UI 寫 → view 層 post-filter → 撞上層錯位。</p>
<h3 id="能用訊號早於對齊資料形狀">「能用」訊號早於「對齊資料形狀」</h3>
<p>寫完 view 層 filter、手動測一次能用、覺得對 — 但能用的範圍是「已載入子集」、不是「完整 dataset」。資料形狀的限制要刻意對照才看得到。</p>
<hr>
<h2 id="多面向資料源形狀的不同類型">多面向：資料源形狀的不同類型</h2>
<h3 id="形狀-1一次性給完整-dataset">形狀 1：一次性給完整 dataset</h3>
<p>範例：靜態 JSON、SSR 完整渲染、API 一次回全集（&lt; 1MB）。</p>
<table>
  <thead>
      <tr>
          <th>Feature 設計</th>
          <th>安全與否</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>任意層 filter</td>
          <td>安全</td>
      </tr>
      <tr>
          <td>任意層 sort</td>
          <td>安全</td>
      </tr>
      <tr>
          <td>Count</td>
          <td>安全</td>
      </tr>
      <tr>
          <td>Pagination</td>
          <td>不需要</td>
      </tr>
  </tbody>
</table>
<p>這類 source 是「最寬容」的、UI 想怎麼設計都行。</p>
<h3 id="形狀-2分批-fetchpagination">形狀 2：分批 fetch（pagination）</h3>
<p>範例：pagefind、infinite scroll、cursor-based API。</p>
<table>
  <thead>
      <tr>
          <th>Feature 設計</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter</td>
          <td>必須跟 source 同層（A）或自動續抓（B）</td>
      </tr>
      <tr>
          <td>Sort</td>
          <td>必須是 server-side sort、不能 client 重排</td>
      </tr>
      <tr>
          <td>Count</td>
          <td>通常需要 source 提供 total（pagefind 有）</td>
      </tr>
      <tr>
          <td>「跳到最後一頁」</td>
          <td>需要 cursor / offset 支援</td>
      </tr>
  </tbody>
</table>
<p>UI 設計時要避開：「立即 filter」「立即 sort」「Show all」 — 這些假設 dataset 已 materialize。</p>
<h3 id="形狀-3streaming--async-iterator">形狀 3：Streaming / async iterator</h3>
<p>範例：SSE、WebSocket push、async iterator from generator、log tail。</p>
<table>
  <thead>
      <tr>
          <th>Feature 設計</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter</td>
          <td>可在 stream 裡做（透明）</td>
      </tr>
      <tr>
          <td>Sort</td>
          <td>不能 — stream 沒終點、無法 sort</td>
      </tr>
      <tr>
          <td>Count</td>
          <td>「目前累計」、不是「總數」</td>
      </tr>
      <tr>
          <td>進度條</td>
          <td>只能顯示「已收 N 筆」、不能 % progress</td>
      </tr>
  </tbody>
</table>
<p>UI 設計時要避開：「sort by 任意欄位」「總共 X 筆」「進度條 50%」 — 這些假設有限終點。</p>
<h3 id="形狀-4cached--revalidate">形狀 4：Cached + revalidate</h3>
<p>範例：service worker cache、SWR、HTTP cache、IndexedDB cache。</p>
<table>
  <thead>
      <tr>
          <th>Feature 設計</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter</td>
          <td>哪個 dataset 在 filter？cache 還是 fresh？</td>
      </tr>
      <tr>
          <td>「最新狀態」訊號</td>
          <td>需要 UI 區分 stale vs fresh</td>
      </tr>
      <tr>
          <td>衝突處理</td>
          <td>Cache 跟 fresh 結果不同時、誰 winning？</td>
      </tr>
  </tbody>
</table>
<p>UI 設計時要決定：cache-first（快但 stale）還是 fresh-first（慢但新）。Filter 跟其他操作要對齊這個選擇。</p>
<hr>
<h2 id="形狀識別的-protocol">形狀識別的 protocol</h2>
<p>拿到一個 source（API、SDK、library）、用以下兩問判斷它是哪個形狀：</p>
<h3 id="問-1是否一次給完整-dataset">問 1：是否一次給完整 dataset？</h3>
<table>
  <thead>
      <tr>
          <th>答案</th>
          <th>形狀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>是</td>
          <td>形狀 1（一次性）— 安全</td>
      </tr>
      <tr>
          <td>否</td>
          <td>形狀 2 / 3 / 4 — 進問 2</td>
      </tr>
  </tbody>
</table>
<p>判讀依據：API 是否有 <code>pagination</code> / <code>cursor</code> / <code>nextPage</code> / <code>loadMore</code> / <code>for await</code> / <code>subscribe</code> 等概念？有就是「不一次給完」。</p>
<h3 id="問-2分批的觸發機制是什麼">問 2：分批的觸發機制是什麼？</h3>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>形狀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶端要求下一頁（pull）</td>
          <td>形狀 2（paginated）</td>
      </tr>
      <tr>
          <td>伺服端推（push）、可能無終點</td>
          <td>形狀 3（streaming）</td>
      </tr>
      <tr>
          <td>預先給一份（cache）+ 之後重抓（fresh）</td>
          <td>形狀 4（cached + revalidate）</td>
      </tr>
  </tbody>
</table>
<p>判讀依據：SDK doc / API spec 的「資料更新方式」段落。讀不到就跑 spike：手動觸發、看是 pull 還是 push、有沒有 cache。</p>
<p>兩問跑完、形狀已知 → 寫 feature 之前能評估「資料形狀對 feature 設計的硬約束」。</p>
<hr>
<h2 id="形狀混合疊加">形狀混合（疊加）</h2>
<p>實務上、source 常常是多個形狀疊加。常見組合：</p>
<h3 id="組合-1cached--paginated">組合 1：Cached + Paginated</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">[Server paginated API]
</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">[Client cache layer (e.g. SWR)]
</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">[UI 拿 cache + 分批 fetch fresh]</span></span></code></pre></div><ul>
<li>形狀 4（cached）+ 形狀 2（paginated）疊加</li>
<li>Filter 要決定：在 cache 上還是 fresh 上？fresh 是分批的、又有層錯位？</li>
</ul>
<h3 id="組合-2streaming--buffered">組合 2：Streaming + Buffered</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">[Server SSE push]
</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">[Client buffer N events]
</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">[UI 從 buffer 取]</span></span></code></pre></div><ul>
<li>形狀 3（streaming）+ 內部 buffer 限額</li>
<li>Filter 要看：在 stream 入口還是 buffer 出口？buffer 滿了怎麼處理舊事件？</li>
</ul>
<h3 id="組合-3lazy-iterator--taken">組合 3：Lazy iterator + take(N)</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">stream</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">remote_paginated</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">yield from</span> <span class="n">chunk</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">list</span><span class="p">(</span><span class="n">itertools</span><span class="o">.</span><span class="n">islice</span><span class="p">(</span><span class="n">stream</span><span class="p">(),</span> <span class="mi">100</span><span class="p">))</span>  <span class="c1"># 限額 100</span></span></span></code></pre></div><ul>
<li>形狀 2（paginated）+ 用 take 限額 → 行為像形狀 1（一次給完）但只給前 100</li>
<li>Filter 全集還是 100 個 subset？</li>
</ul>
<p>混合形狀的 filter 要分別處理每一層的層錯位、不是當成單一形狀。</p>
<hr>
<h2 id="形狀的可改造性">形狀的可改造性</h2>
<p>形狀不只決定 feature 設計、還決定「策略可選範圍」。可改造性分三類：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>例子</th>
          <th>對策略選擇的影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>你控的 source</td>
          <td>自家 build pipeline、自家 API</td>
          <td>全部策略可選（A 重 index、C 多 index、改 schema 都行）</td>
      </tr>
      <tr>
          <td>你不控但能要求</td>
          <td>同公司其他團隊、open source vendor</td>
          <td>部分可選（提 issue / PR、等回覆）</td>
      </tr>
      <tr>
          <td>完全不可控</td>
          <td>第三方 API、legacy black box</td>
          <td>只剩 B / D / E（client-side 解）</td>
      </tr>
  </tbody>
</table>
<p>評估可改造性、跟 #59 五策略的選擇配套：</p>
<ul>
<li>全可控 → A（推進 query）或 C（多 index）通常最優</li>
<li>半可控 → B 短期解 + 長期等可改造</li>
<li>不可控 → 接受 D / E、不要硬撞 A / C</li>
</ul>
<hr>
<h2 id="寫-feature-前的形狀對照表">寫 feature 前的形狀對照表</h2>
<p>寫第一行之前、先填這張表：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>答案</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 是什麼形狀（1-4）</td>
          <td>?</td>
      </tr>
      <tr>
          <td>Total cardinality 是多少</td>
          <td>?（10? 1萬? 10萬?）</td>
      </tr>
      <tr>
          <td>是否分批 / 限額 / streaming</td>
          <td>?</td>
      </tr>
      <tr>
          <td>Source 支援哪些 filter / sort</td>
          <td>?</td>
      </tr>
      <tr>
          <td>Cache 策略（如果有）</td>
          <td>?</td>
      </tr>
      <tr>
          <td>Match 密度預期</td>
          <td>?（密集 / 中等 / 稀疏）</td>
      </tr>
  </tbody>
</table>
<p>填完後評估：UI 設計需求跟資料形狀有沒有衝突？衝突就重設計 UI、或調整資料層、或退到誠實 UX（D）。</p>
<hr>
<h2 id="設計取捨ui-還是-source-先服從">設計取捨：UI 還是 Source 先服從</h2>
<h3 id="aui-服從-source-形狀推薦">A：UI 服從 source 形狀（推薦）</h3>
<ul>
<li><strong>機制</strong>：先看 source 給什麼形狀、UI 設計成「這個形狀能呈現的」</li>
<li><strong>適合</strong>：source 已存在（vendor library、legacy API、無法改）</li>
<li><strong>代價</strong>：UI 可能比設計理想中簡單</li>
</ul>
<h3 id="bsource-服從-ui-需求重設計-source">B：Source 服從 UI 需求（重設計 source）</h3>
<ul>
<li><strong>機制</strong>：UI 設計理想化、為了支援 UI、改 source（重 index、加欄位、換 SDK）</li>
<li><strong>跟 A 的取捨</strong>：B 工程量大、但 UX 上限高</li>
<li><strong>B 才合理的情境</strong>：source 能控、改 source 的成本 &lt; 長期 UX 收益</li>
</ul>
<h3 id="c兩邊妥協用誠實-ux-補縫">C：兩邊妥協、用誠實 UX 補縫</h3>
<ul>
<li><strong>機制</strong>：UI 設計理想、source 不重做、用 #62 誠實進度 UX 把資料形狀的限制告訴使用者</li>
<li><strong>跟 A 的取捨</strong>：C 比 A 顯眼、比 B 工程量小、是常見的中間方案</li>
<li><strong>C 才合理的情境</strong>：使用者能接受顯眼的「掃描範圍」UX</li>
</ul>
<h3 id="dui-假裝-source-形狀符合反模式">D：UI 假裝 source 形狀符合（反模式）</h3>
<ul>
<li><strong>為什麼是反模式</strong>：UI 暗示的能力跟資料層實際能力不符、使用者基於錯誤訊號決策</li>
<li><strong>看起來吸引人的原因</strong>：UI 設計可以理想化、不用看資料層限制、設計師跟工程師都輕鬆</li>
<li><strong>實際發生的代價</strong>：撞上 <a href="../view-layer-filter-vs-source-layer/">#55 層錯位</a>、長期維護負擔大（每次 source 升級都要重 patch）、使用者信任損失</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>拿到 wireframe 開始實作前、沒看過資料源 API doc</td>
          <td>先看 — 確認資料形狀</td>
      </tr>
      <tr>
          <td>UI 含「立即 filter」「sort by 任意欄位」但 source 是分批的</td>
          <td>衝突 — 重設計 UI 或重 index source</td>
      </tr>
      <tr>
          <td>UI 顯示 progress bar 但 source 是 streaming</td>
          <td>衝突 — 改成「已收 N 筆」、不寫 %</td>
      </tr>
      <tr>
          <td>Cache 策略沒設定就開始寫 feature</td>
          <td>先設定 — cache-first / fresh-first</td>
      </tr>
      <tr>
          <td>內心 OS：「資料層之後處理、先把 UI 寫出來」</td>
          <td>停 — 形狀對照表先填</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：資料源的形狀是 feature 的硬約束。UI 設計可以理想化、但實作要看 source 給什麼。憑 UI 倒推資料層的實作 = 在錯誤的層解錯誤的問題、最終產生層錯位類 bug。</p>
<p>「形狀的可改造性」三類跟 <a href="../external-component-customization/">#1 在外部組件上加客製功能</a> 共骨：兩者都是「先看你能改什麼、再決定怎麼客製」。#1 講的是 UI 客製、本卡講的是資料層客製、共同精神是「客製從邊界往中心做、不要倒推」。</p>
]]></content:encoded></item><item><title>Feature 操作要跟 Source 同層合成</title><link>https://tarrragon.github.io/blog/report/compose-feature-at-source-layer/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/compose-feature-at-source-layer/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Stream 操作（filter / sort / count / transform / search）必須跟 stream 的 materialization 同層或更上游合成。&lt;/strong> 在下游合成 = 操作的對象是 subset、不是 stream。&lt;/p>
&lt;p>這是 #55 &lt;a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位&lt;/a> 的抽象升級 — 不限於「視覺層 vs 資料層」、適用任何分層系統（前端 / 後端 / 演算法管線 / 資料庫）。&lt;/p>
&lt;hr>
&lt;h2 id="抽象結構">抽象結構&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[Stream Source]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ (materialize 部分)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">[Subset L1]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓ (再 materialize)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">[Subset L2]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> ↓ ...&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Stream 操作要套在哪一層、決定它「過濾的範圍」是什麼：&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>Stream Source&lt;/td>
 &lt;td>完整 stream&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Subset L1&lt;/td>
 &lt;td>L1 子集&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Subset L2&lt;/td>
 &lt;td>L1 的子集的子集&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>使用者 / 呼叫者通常想要的是「完整 stream 的操作結果」、不是「下游 subset 的結果」。在下游做 = 跟意圖不對齊。&lt;/p>
&lt;hr>
&lt;h2 id="多面向跨領域的同個結構">多面向：跨領域的同個結構&lt;/h2>
&lt;h3 id="領域-1前端-ui55-的-case">領域 1：前端 UI（#55 的 case）&lt;/h3>
&lt;ul>
&lt;li>Stream：完整搜尋結果集&lt;/li>
&lt;li>Materialize：pagefind 分批 fetch&lt;/li>
&lt;li>Subset：已載入的 result&lt;/li>
&lt;li>錯誤合成：在 view 層 filter（subset 上做）&lt;/li>
&lt;/ul>
&lt;h3 id="領域-2後端-api--middleware">領域 2：後端 API + middleware&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">[Database query result] ← stream source
&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">[ORM materialize as objects] ← L1 subset (lazy load 部分欄位)
&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">[API response] ← L2 subset (pagination 後)
&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">[Middleware filter] ← 錯誤位置 — 已是 subset 了&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Middleware 過濾「pagination 後的回應」 — 漏掉沒在這頁的符合項。應該推進 ORM query。&lt;/p>
&lt;h3 id="領域-3演算法管線">領域 3：演算法管線&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">pipeline&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">chunk&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">load_chunks&lt;/span>&lt;span class="p">():&lt;/span> &lt;span class="c1"># stream source&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">item&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">chunk&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="c1"># L1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">processed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">transform&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># L2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">yield&lt;/span> &lt;span class="n">processed&lt;/span> &lt;span class="c1"># L3&lt;/span>
&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 class="c1"># 錯誤合成&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pipeline&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n">filtered&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">x&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">x&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">results&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">matches&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&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="c1"># ↑ 如果上游有 take(N) 或 break、filtered 對的是 subset&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對例：filter 推到 transform 之前 / 之內。&lt;/p>
&lt;h3 id="領域-4資料庫--materialized-view">領域 4：資料庫 + materialized view&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 錯誤：在 view 上 filter
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">materialized_view&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- ↑ materialized_view 可能是 partial / stale
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 對例：filter 推進原表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">source_table&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 或 view 重建時 filter 已加進去&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="領域-5map--reduce">領域 5：Map / Reduce&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">[shards] → [map output partial] → [reduce]
&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"> [post-reduce filter] ← 錯位&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Filter 應該在 map 階段（per-shard）或 reduce 內、不是 reduce 後。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Stream 操作（filter / sort / count / transform / search）必須跟 stream 的 materialization 同層或更上游合成。</strong> 在下游合成 = 操作的對象是 subset、不是 stream。</p>
<p>這是 #55 <a href="../view-layer-filter-vs-source-layer/">Filter 與 Source 的層錯位</a> 的抽象升級 — 不限於「視覺層 vs 資料層」、適用任何分層系統（前端 / 後端 / 演算法管線 / 資料庫）。</p>
<hr>
<h2 id="抽象結構">抽象結構</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[Stream Source]
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ (materialize 部分)
</span></span><span class="line"><span class="ln">3</span><span class="cl">[Subset L1]
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ (再 materialize)
</span></span><span class="line"><span class="ln">5</span><span class="cl">[Subset L2]
</span></span><span class="line"><span class="ln">6</span><span class="cl">   ↓ ...</span></span></code></pre></div><p>Stream 操作要套在哪一層、決定它「過濾的範圍」是什麼：</p>
<table>
  <thead>
      <tr>
          <th>套在哪一層</th>
          <th>操作範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stream Source</td>
          <td>完整 stream</td>
      </tr>
      <tr>
          <td>Subset L1</td>
          <td>L1 子集</td>
      </tr>
      <tr>
          <td>Subset L2</td>
          <td>L1 的子集的子集</td>
      </tr>
  </tbody>
</table>
<p>使用者 / 呼叫者通常想要的是「完整 stream 的操作結果」、不是「下游 subset 的結果」。在下游做 = 跟意圖不對齊。</p>
<hr>
<h2 id="多面向跨領域的同個結構">多面向：跨領域的同個結構</h2>
<h3 id="領域-1前端-ui55-的-case">領域 1：前端 UI（#55 的 case）</h3>
<ul>
<li>Stream：完整搜尋結果集</li>
<li>Materialize：pagefind 分批 fetch</li>
<li>Subset：已載入的 result</li>
<li>錯誤合成：在 view 層 filter（subset 上做）</li>
</ul>
<h3 id="領域-2後端-api--middleware">領域 2：後端 API + middleware</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">[Database query result]  ← stream source
</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">[ORM materialize as objects]  ← L1 subset (lazy load 部分欄位)
</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">[API response]  ← L2 subset (pagination 後)
</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">[Middleware filter]  ← 錯誤位置 — 已是 subset 了</span></span></code></pre></div><p>Middleware 過濾「pagination 後的回應」 — 漏掉沒在這頁的符合項。應該推進 ORM query。</p>
<h3 id="領域-3演算法管線">領域 3：演算法管線</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">pipeline</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">load_chunks</span><span class="p">():</span>       <span class="c1"># stream source</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">chunk</span><span class="p">:</span>             <span class="c1"># L1</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="n">processed</span> <span class="o">=</span> <span class="n">transform</span><span class="p">(</span><span class="n">item</span><span class="p">)</span> <span class="c1"># L2</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="k">yield</span> <span class="n">processed</span>             <span class="c1"># L3</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 錯誤合成</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">pipeline</span><span class="p">())</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">filtered</span> <span class="o">=</span> <span class="p">[</span><span class="n">x</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">results</span> <span class="k">if</span> <span class="n">matches</span><span class="p">(</span><span class="n">x</span><span class="p">)]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># ↑ 如果上游有 take(N) 或 break、filtered 對的是 subset</span></span></span></code></pre></div><p>對例：filter 推到 transform 之前 / 之內。</p>
<h3 id="領域-4資料庫--materialized-view">領域 4：資料庫 + materialized view</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 錯誤：在 view 上 filter
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">materialized_view</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="c1">-- ↑ materialized_view 可能是 partial / stale
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 對例：filter 推進原表
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">source_table</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="c1">-- 或 view 重建時 filter 已加進去</span></span></span></code></pre></div><h3 id="領域-5map--reduce">領域 5：Map / Reduce</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">[shards] → [map output partial] → [reduce]
</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">                                  [post-reduce filter]  ← 錯位</span></span></code></pre></div><p>Filter 應該在 map 階段（per-shard）或 reduce 內、不是 reduce 後。</p>
<p><strong>五個領域共用結構</strong>：在 materialization 下游做 stream 操作 → silent 缺口。</p>
<hr>
<h2 id="同層合成的具體做法">同層合成的具體做法</h2>
<h3 id="做法-1把操作推進-source-query">做法 1：把操作推進 source query</h3>
<p>最直接 — source 端就回符合的、根本沒 subset。</p>
<p>對應 #61 <a href="../pattern-query-side-pushdown/">Pattern：推進 query</a>。</p>
<h3 id="做法-2在-materialization-過程中合成">做法 2：在 materialization 過程中合成</h3>
<p>如果 source 是 lazy stream、操作放進 stream 而不是事後：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 對例：filter 放進 stream</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">filtered_pipeline</span><span class="p">(</span><span class="n">predicate</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">load_chunks</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">chunk</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="n">predicate</span><span class="p">(</span><span class="n">item</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">                <span class="k">yield</span> <span class="n">item</span></span></span></code></pre></div><p>每筆 materialize 時就 filter、不累積到 subset 後再做。</p>
<h3 id="做法-3自動續抓直到湊滿">做法 3：自動續抓直到湊滿</h3>
<p>當 source 不能改、且 materialization 是分批 — 用 loop 把分批變透明。</p>
<p>對應 #60 <a href="../pattern-fetch-until-quota/">Pattern：自動續抓</a>。</p>
<h3 id="做法-4明示降級到-subset-操作">做法 4：明示降級到 subset 操作</h3>
<p>不能同層合成 → 顯式告訴呼叫者「我只在 subset 上做」、而不是假裝在 stream 上做。</p>
<p>對應 #62 <a href="../pattern-honest-progress-ui/">Pattern：誠實進度 UX</a>。</p>
<hr>
<h2 id="為什麼這個原則跨領域通用資訊可見範圍">為什麼這個原則跨領域通用：資訊可見範圍</h2>
<p>五個領域共用結構不是巧合。底層命題是<strong>資訊論的問題、不是工程問題</strong>：</p>
<blockquote>
<p>一個操作能「看見」的範圍、就是它能正確套用的範圍。把操作放在看不見完整 stream 的位置 = 操作對部分資訊運算 = 結果不能宣稱對完整資訊。</p></blockquote>
<p>「合成位置」就是「資訊可見範圍」的代名詞。同層或上游的位置看得到完整 stream、下游位置只看得到 subset。這跟「stream 是什麼樣的資料」「系統是哪個語言寫的」「框架是 React 還是 Vue」都無關 — 只跟「看得到什麼」有關。</p>
<p>所以這個原則：</p>
<ul>
<li>不是「前端 bug」 — 後端、演算法、DB、map-reduce、分散式系統都會遇到</li>
<li>不是「特定技術 stack 問題」 — 任何分層架構都適用</li>
<li>不是「pagefind 特定問題」 — 任何「分批 materialize」的 source 都會引發</li>
</ul>
<p>把它當「資訊可見範圍」原則來理解、能應用到任何「stream 操作 + 分層 materialization」的情境。</p>
<hr>
<h2 id="上推push-down在不同領域的代價">上推（push down）在不同領域的代價</h2>
<p>把操作從下游推到上游 = 改變誰負責執行操作。每個領域的「上推」代價不同：</p>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>上推 = 在哪裡做</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>前端 UI</td>
          <td>推到 fetch 層 / source query</td>
          <td>重設計 fetcher、可能改 API contract</td>
      </tr>
      <tr>
          <td>後端 middleware</td>
          <td>推到 ORM query / SQL WHERE</td>
          <td>改 query、可能要加 index</td>
      </tr>
      <tr>
          <td>演算法管線</td>
          <td>推到 stream stage 內</td>
          <td>重排 pipeline、可能影響其他 stage</td>
      </tr>
      <tr>
          <td>資料庫</td>
          <td>推到原表 query / 重建 view</td>
          <td>重 build view、影響其他依賴 view 的 query</td>
      </tr>
      <tr>
          <td>Map-reduce</td>
          <td>推到 map 階段或 reduce 內</td>
          <td>改 mapper / reducer 邏輯</td>
      </tr>
  </tbody>
</table>
<p>代價評估決定「能不能上推」：</p>
<ul>
<li>代價 &lt; 缺口的維護成本 → 上推</li>
<li>代價 &gt; 缺口的維護成本 → 退到 explicit 縮小（#66）+ 接受</li>
<li>代價 ≈ 缺口的維護成本 → 看其他因素（短期 vs 長期、團隊熟悉度）</li>
</ul>
<hr>
<h2 id="常見誤判以為自己在-source-層實際在-subset-層">常見誤判：以為自己在 source 層、實際在 subset 層</h2>
<p>每個領域都有「看起來是 source 但實際是 subset」的陷阱：</p>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>看起來是 source、實際是 subset</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>前端</td>
          <td><code>Array.from(document.querySelectorAll(...))</code> 看起來是「全部元素」、實際是「已 render 的元素」</td>
      </tr>
      <tr>
          <td>後端 ORM</td>
          <td><code>User.all()</code> 看起來是「所有 user」、實際是 lazy load + memory 限制</td>
      </tr>
      <tr>
          <td>演算法</td>
          <td><code>list(generator)</code> 看起來是「materialize 全部」、實際 generator 上游可能 lazy / take(N)</td>
      </tr>
      <tr>
          <td>資料庫</td>
          <td><code>SELECT * FROM materialized_view</code> 看起來是查表、實際 view 可能 stale / partial</td>
      </tr>
      <tr>
          <td>分散式 cache</td>
          <td><code>cache.get_all()</code> 看起來是「cache 全集」、實際是 single-node subset</td>
      </tr>
  </tbody>
</table>
<p>這些誤判共用結構：<strong>API 命名暗示「全集」、實際是 subset</strong>。寫之前要看「這個 API 的真實 cardinality 是什麼」、不是看名字。</p>
<hr>
<h2 id="跟-63-形狀原則的關係">跟 #63 形狀原則的關係</h2>
<p><a href="../data-source-shape-defines-feature-shape/">#63 資料源的形狀決定 feature 的形狀</a> 講「形狀是硬約束」 — 本文講「在硬約束下、操作該放哪一層」。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>#63</th>
          <th>本文</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>焦點</td>
          <td>形狀如何約束 feature 設計</td>
          <td>操作如何跟 stream 合成</td>
      </tr>
      <tr>
          <td>階段</td>
          <td>設計 / 規劃</td>
          <td>實作 / 架構</td>
      </tr>
      <tr>
          <td>結論</td>
          <td>不要憑 UI 倒推資料層</td>
          <td>操作要同層或更上游</td>
      </tr>
  </tbody>
</table>
<p>兩者互補：#63 是 high-level 設計原則、本文是 implementation 指引。</p>
<hr>
<h2 id="設計取捨操作合成的位置">設計取捨：操作合成的位置</h2>
<p>四種、跟 #59 <a href="../filter-source-composition-strategies/">策略五選一</a> 對應但更抽象。</p>
<h3 id="a合成在-source">A：合成在 source</h3>
<p>最近 stream、無 silent 缺口。對應 #61 推進 query。</p>
<h3 id="b合成在-materialization-過程中">B：合成在 materialization 過程中</h3>
<p>Stream 處理時就做、不累積到 subset 後。對應 #60 自動續抓 + 在 loop 內 filter。</p>
<h3 id="c合成在-subset但顯式">C：合成在 subset、但顯式</h3>
<p>明示語意縮小、用誠實 UX 告訴呼叫者範圍。對應 #62。</p>
<h3 id="d合成在-subset隱式反模式">D：合成在 subset、隱式（反模式）</h3>
<ul>
<li><strong>為什麼是反模式</strong>：silent 失敗、跟意圖有縫、違反「資訊可見範圍 = 操作正確套用範圍」的本質</li>
<li><strong>看起來吸引人的原因</strong>：寫起來最快、用現成 subset、不用追上游、5 行解決</li>
<li><strong>實際發生的代價</strong>：跨情境 silent bug、使用者基於錯結果決策、debug 時定位困難（因為錯位的位置不會報錯）</li>
</ul>
<p>選擇順序：<strong>A → B → C → 不要 D</strong>。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫 <code>.filter()</code> / <code>.sort()</code> / <code>.count()</code> 在已 materialize 的 subset 上</td>
          <td>確認 source 是不是 stream / 分批；是 → 推到上游</td>
      </tr>
      <tr>
          <td>跨多層的系統、操作出現在最下游</td>
          <td>評估能不能上推</td>
      </tr>
      <tr>
          <td>「能用、但沒覆蓋邊界 case」的功能</td>
          <td>多半是合成位置錯了</td>
      </tr>
      <tr>
          <td>Map-reduce / pipeline / middleware 鏈路裡、filter 在最後一層</td>
          <td>推進到 stage 內</td>
      </tr>
      <tr>
          <td>內心 OS：「在最後 filter 比較容易寫」</td>
          <td>是訊號 — 容易寫的位置通常是錯位的位置</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Stream 操作的合成位置決定它的語意。同層或更上游 = 操作 stream、跟意圖對齊。下游 = 操作 subset、跟意圖有縫。這個原則跨前端 / 後端 / 演算法 / 資料庫 / 分散式系統通用 — 不是「前端 vs 後端」的問題、是「合成位置 vs materialization 位置」的問題。</p>
<p>跟其他抽象層原則的關係：</p>
<ul>
<li>跟 <a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍是 sanity 防線</a>：兩者共用「邊界選對 vs 選錯」的精神 — #43 講範圍從窄到寬、本卡講合成從上游到下游；錯方向都是 silent 失敗</li>
<li>跟 <a href="../single-source-of-truth/">#44 Single Source of Truth</a>：兩者共用「值的住址唯一」精神 — SSOT 是「定義位置唯一」、本卡是「操作位置正確」；操作不在 source 層 = 等於建了個第二定義（subset 上的「filter 結果」）跟 stream 全集競爭</li>
<li>跟 <a href="../two-occurrence-threshold/">#42 2 次門檻</a>：發現合成位置錯時、不要試「同層補丁」三次以上、第 2 次失敗就退一層找根因</li>
</ul>
]]></content:encoded></item><item><title>Pattern：預先建獨立 index（每種 mode 一份）</title><link>https://tarrragon.github.io/blog/report/pattern-multiple-indexes/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-multiple-indexes/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>Build time 為每種 filter mode 各建一份獨立 source / index、runtime 切換 mode 等於切 source。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 C。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>能控 source 的 build pipeline（自家 build、不是第三方 API）&lt;/li>
&lt;li>Filter mode 數量有限且穩定（&amp;lt; 5 個、不會爆炸組合）&lt;/li>
&lt;li>兩個（含以上）mode 都重要、流量大、值得獨立 index&lt;/li>
&lt;li>Source 的 query 引擎不支援該 filter（不能用 #61 推進 query）&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Filter 維度多、組合會爆炸（5 維 × 各 5 選項 = 3125 種 index）&lt;/li>
&lt;li>Index 大小敏感（每份 index 都重複占空間）&lt;/li>
&lt;li>Build pipeline 無法控（外部 API、vendor service）&lt;/li>
&lt;li>Mode 不穩定、常常增刪&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="結構">結構&lt;/h2>
&lt;h3 id="build-pipeline">Build pipeline&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># Pagefind 範例：兩份 index 各自掃不同 region&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pagefind --site public --output-subdir _pagefind-all
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">pagefind --site public/title-only --output-subdir _pagefind-title
&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 class="c1"># 或用 --root-selector 限定 source 範圍&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">pagefind --site public --root-selector &lt;span class="s2">&amp;#34;.post-title&amp;#34;&lt;/span> --output-subdir _pagefind-title&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="runtime-切換">Runtime 切換&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">indexes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">all&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="kr">import&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/_pagefind-all/pagefind.js&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">title&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="kr">import&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;/_pagefind-title/pagefind.js&amp;#39;&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>&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 class="kd">function&lt;/span> &lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">mode&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">7&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">indexes&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">mode&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個 mode 對應一份完整 index、search 結果直接是該 mode 的全集。&lt;strong>沒有 post-filter、沒有層錯位&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="多-index-的成本">多 index 的成本&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>Build 時間&lt;/td>
 &lt;td>每份 index 各建、線性增加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存空間&lt;/td>
 &lt;td>每份各自占用（pagefind 約 site 大小 2-5%）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>載入頻寬&lt;/td>
 &lt;td>runtime 載入哪份 = 該 mode 的 size&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維護&lt;/td>
 &lt;td>改 source / schema 時、所有 index 都要重 build&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>通常 build / 儲存的成本 &amp;lt; 在 runtime 自動續抓（B）的累積請求成本。&lt;/p>
&lt;hr>
&lt;h2 id="跟-59-策略並用">跟 #59 策略並用&lt;/h2>
&lt;p>C 通常跟其他策略並用：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>C + 推進 query (A)&lt;/strong>：在每份 index 內、再用 query filter 細分（如 &lt;code>_pagefind-post.search('css', { filters: { tag: 'js' } })&lt;/code>）&lt;/li>
&lt;li>&lt;strong>C 切 mode + B 自動續抓&lt;/strong>：mode 切換無感、mode 內續抓也無感&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="反例">反例&lt;/h2>
&lt;h3 id="反例-1mode-組合爆炸">反例 1：Mode 組合爆炸&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 5 維 × 各 5 選項 = 3125 份 index&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nb">type&lt;/span> in post page tutorial faq doc&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> tag in js css html ts py&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">for&lt;/span> date in &lt;span class="m">2020&lt;/span> &lt;span class="m">2021&lt;/span> &lt;span class="m">2022&lt;/span> &lt;span class="m">2023&lt;/span> 2024&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> pagefind --filter &lt;span class="s2">&amp;#34;type=&lt;/span>&lt;span class="nv">$type&lt;/span>&lt;span class="s2"> tag=&lt;/span>&lt;span class="nv">$tag&lt;/span>&lt;span class="s2"> date=&lt;/span>&lt;span class="nv">$date&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --output-subdir _pagefind-&lt;span class="nv">$type&lt;/span>-&lt;span class="nv">$tag&lt;/span>-&lt;span class="nv">$date&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>組合爆炸時不能用 C — 改用 A（推進 query）讓 source 一份就好。&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>Build time 為每種 filter mode 各建一份獨立 source / index、runtime 切換 mode 等於切 source。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 C。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>能控 source 的 build pipeline（自家 build、不是第三方 API）</li>
<li>Filter mode 數量有限且穩定（&lt; 5 個、不會爆炸組合）</li>
<li>兩個（含以上）mode 都重要、流量大、值得獨立 index</li>
<li>Source 的 query 引擎不支援該 filter（不能用 #61 推進 query）</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Filter 維度多、組合會爆炸（5 維 × 各 5 選項 = 3125 種 index）</li>
<li>Index 大小敏感（每份 index 都重複占空間）</li>
<li>Build pipeline 無法控（外部 API、vendor service）</li>
<li>Mode 不穩定、常常增刪</li>
</ul>
<hr>
<h2 id="結構">結構</h2>
<h3 id="build-pipeline">Build pipeline</h3>





<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"># Pagefind 範例：兩份 index 各自掃不同 region</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pagefind --site public --output-subdir _pagefind-all
</span></span><span class="line"><span class="ln">3</span><span class="cl">pagefind --site public/title-only --output-subdir _pagefind-title
</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"># 或用 --root-selector 限定 source 範圍</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">pagefind --site public --root-selector <span class="s2">&#34;.post-title&#34;</span> --output-subdir _pagefind-title</span></span></code></pre></div><h3 id="runtime-切換">Runtime 切換</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="kr">const</span> <span class="nx">indexes</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">all</span><span class="o">:</span> <span class="kr">await</span> <span class="kr">import</span><span class="p">(</span><span class="s1">&#39;/_pagefind-all/pagefind.js&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">title</span><span class="o">:</span> <span class="kr">await</span> <span class="kr">import</span><span class="p">(</span><span class="s1">&#39;/_pagefind-title/pagefind.js&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="kd">function</span> <span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="nx">mode</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">return</span> <span class="nx">indexes</span><span class="p">[</span><span class="nx">mode</span><span class="p">].</span><span class="nx">search</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>每個 mode 對應一份完整 index、search 結果直接是該 mode 的全集。<strong>沒有 post-filter、沒有層錯位</strong>。</p>
<hr>
<h2 id="多-index-的成本">多 index 的成本</h2>
<table>
  <thead>
      <tr>
          <th>成本面</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build 時間</td>
          <td>每份 index 各建、線性增加</td>
      </tr>
      <tr>
          <td>儲存空間</td>
          <td>每份各自占用（pagefind 約 site 大小 2-5%）</td>
      </tr>
      <tr>
          <td>載入頻寬</td>
          <td>runtime 載入哪份 = 該 mode 的 size</td>
      </tr>
      <tr>
          <td>維護</td>
          <td>改 source / schema 時、所有 index 都要重 build</td>
      </tr>
  </tbody>
</table>
<p>通常 build / 儲存的成本 &lt; 在 runtime 自動續抓（B）的累積請求成本。</p>
<hr>
<h2 id="跟-59-策略並用">跟 #59 策略並用</h2>
<p>C 通常跟其他策略並用：</p>
<ul>
<li><strong>C + 推進 query (A)</strong>：在每份 index 內、再用 query filter 細分（如 <code>_pagefind-post.search('css', { filters: { tag: 'js' } })</code>）</li>
<li><strong>C 切 mode + B 自動續抓</strong>：mode 切換無感、mode 內續抓也無感</li>
</ul>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1mode-組合爆炸">反例 1：Mode 組合爆炸</h3>





<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"># 5 維 × 各 5 選項 = 3125 份 index</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">for</span> <span class="nb">type</span> in post page tutorial faq doc<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">for</span> tag in js css html ts py<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">for</span> date in <span class="m">2020</span> <span class="m">2021</span> <span class="m">2022</span> <span class="m">2023</span> 2024<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">      pagefind --filter <span class="s2">&#34;type=</span><span class="nv">$type</span><span class="s2"> tag=</span><span class="nv">$tag</span><span class="s2"> date=</span><span class="nv">$date</span><span class="s2">&#34;</span> --output-subdir _pagefind-<span class="nv">$type</span>-<span class="nv">$tag</span>-<span class="nv">$date</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">done</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>組合爆炸時不能用 C — 改用 A（推進 query）讓 source 一份就好。</p>
<h3 id="反例-2mode-不穩定常常增減">反例 2：Mode 不穩定、常常增減</h3>
<p>每加一個 mode、build pipeline 多一份、deploy 多一份。如果 mode 半年內會大改、不適合 C。</p>
<h3 id="反例-3index-沒對齊-mode">反例 3：Index 沒對齊 mode</h3>





<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">pagefind --site public --output-subdir _pagefind-title
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># build 完後沒過濾、其實 index 了 title + content 全部</span></span></span></code></pre></div><p>如果只是改 output 路徑、沒改 index 範圍 → 兩份 index 內容一樣、白做。要用 <code>--root-selector</code> 或 <code>data-pagefind-body</code> 標記正確範圍。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<p>選擇順序：A → C → B → D（見 #61）：</p>
<ul>
<li>A 不行（source 不支援該 filter） → 評估 C</li>
<li>C 不行（mode 爆炸 / 不能控 build） → 退到 B</li>
<li>B 不行（match 稀疏會爆） → 退到 D</li>
</ul>
<p><strong>C 是 A 的 build-time 模擬</strong> — 用 build 時間換 runtime 體驗、跟使用者意圖完全對齊（每份 index = 該 mode 的全集）。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 不支援該 filter、想用 A 但做不到</td>
          <td>評估能不能控 build → 是 → C</td>
      </tr>
      <tr>
          <td>Mode 數量 &lt; 5、stable、能控 build</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>Mode 組合會爆炸（多維 × 多選）</td>
          <td>不要用 C、考慮 A 或重新思考 mode 設計</td>
      </tr>
      <tr>
          <td>兩份 index 內容一樣（沒對齊 mode）</td>
          <td>Build pipeline 出錯、檢查 source 過濾</td>
      </tr>
      <tr>
          <td>Build 時間翻倍但 runtime 體驗沒改善</td>
          <td>重評估：是否值得多份 index</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：C 用 build-time 換 runtime 體驗。前提是 mode 有限、可控、值得 — 否則退到 A 或 B。</p>
]]></content:encoded></item><item><title>Pattern：明示語意縮小（不承諾全集）</title><link>https://tarrragon.github.io/blog/report/pattern-explicit-semantic-narrowing/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-explicit-semantic-narrowing/</guid><description>&lt;h2 id="pattern-一句話">Pattern 一句話&lt;/h2>
&lt;p>當 filter 必然只在已載入子集上運作、用 UI 文字 / API contract / docstring 明確告訴呼叫者「範圍 = 已載入、不承諾全集」 — 不假裝是全集 filter。&lt;/p>
&lt;p>對應 #59 &lt;a href="../filter-source-composition-strategies/">Filter × Source 合成策略&lt;/a> 的策略 E。&lt;/p>
&lt;hr>
&lt;h2 id="何時用何時不用">何時用、何時不用&lt;/h2>
&lt;h3 id="用">用&lt;/h3>
&lt;ul>
&lt;li>Source 不支援推進 query (A 不可行)&lt;/li>
&lt;li>不能控 build pipeline (C 不可行)&lt;/li>
&lt;li>Match 稀疏、自動續抓會拉爆 (B 不可行)&lt;/li>
&lt;li>工程量限制、做不了 #62 誠實 UX 的三數字&lt;/li>
&lt;li>能接受「filter 範圍 = subset」這個語意縮小、但要使用者知道&lt;/li>
&lt;/ul>
&lt;h3 id="不用">不用&lt;/h3>
&lt;ul>
&lt;li>Source 一次給完整 dataset（沒有 subset、不需要縮小）&lt;/li>
&lt;li>使用者預期 filter 是「全集」、無法接受縮小&lt;/li>
&lt;li>應用情境影響重大決策（finance、medical 等不能接受 silent 範圍縮小）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="跟策略-d誠實-ux的差別">跟策略 D（誠實 UX）的差別&lt;/h2>
&lt;p>D 跟 E 都是「在 subset 上 filter」、差別在「怎麼告訴使用者」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>D（誠實 UX）&lt;/th>
 &lt;th>E（明示語意縮小）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>範圍訊號&lt;/td>
 &lt;td>即時數字（已掃 N / 命中 K）&lt;/td>
 &lt;td>文字描述（一次性告知）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UI 顯眼度&lt;/td>
 &lt;td>高 — 每次都看得到&lt;/td>
 &lt;td>低 — 看一次就過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工程量&lt;/td>
 &lt;td>中 — 要實作三數字&lt;/td>
 &lt;td>低 — 改文字 / 加 docstring&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者參與&lt;/td>
 &lt;td>點「再掃一批」續抓&lt;/td>
 &lt;td>不續抓、自己判斷要不要 load more&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合&lt;/td>
 &lt;td>filter 是主要互動模式&lt;/td>
 &lt;td>filter 是次要功能、原型期&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>簡言之：D 是「持續顯示掃描範圍」、E 是「告訴一次、之後不再提」。&lt;/p>
&lt;hr>
&lt;h2 id="明示的具體做法">「明示」的具體做法&lt;/h2>
&lt;h3 id="ui-明示">UI 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search&amp;#34;&lt;/span> &lt;span class="na">placeholder&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;Filter loaded results...&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">small&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>只在已載入的結果裡篩選。要看更多請先載入更多。&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">small&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「Filter loaded results」、「已載入的結果裡」、「載入更多」 — 三個 cue 讓使用者知道範圍。&lt;/p>
&lt;h3 id="api-contract-明示">API contract 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ts" data-lang="ts">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="cm">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="cm"> * Filter loaded results by predicate.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="cm"> *
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="cm"> * NOTE: Operates on currently loaded subset only.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="cm"> * Does NOT trigger fetch of un-loaded items. To filter the full
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="cm"> * dataset, use {@link searchAll} instead.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="cm"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">filterLoaded&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">predicate&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">item&lt;/span>: &lt;span class="kt">Item&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="kr">boolean&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">Item&lt;/span>&lt;span class="p">[];&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>JSDoc / TSDoc 把語意寫進 API、IDE 提示能看到。&lt;/p>
&lt;h3 id="docstring--readme-明示">Docstring / README 明示&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gu">## Filter behavior
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="sb">`filter()`&lt;/span> only operates on results currently loaded in client.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">If the source uses pagination, items not yet loaded are NOT included.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">For full-dataset filtering, the source must support server-side filter.&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>文件級的明示、給開發者讀。&lt;/p></description><content:encoded><![CDATA[<h2 id="pattern-一句話">Pattern 一句話</h2>
<p>當 filter 必然只在已載入子集上運作、用 UI 文字 / API contract / docstring 明確告訴呼叫者「範圍 = 已載入、不承諾全集」 — 不假裝是全集 filter。</p>
<p>對應 #59 <a href="../filter-source-composition-strategies/">Filter × Source 合成策略</a> 的策略 E。</p>
<hr>
<h2 id="何時用何時不用">何時用、何時不用</h2>
<h3 id="用">用</h3>
<ul>
<li>Source 不支援推進 query (A 不可行)</li>
<li>不能控 build pipeline (C 不可行)</li>
<li>Match 稀疏、自動續抓會拉爆 (B 不可行)</li>
<li>工程量限制、做不了 #62 誠實 UX 的三數字</li>
<li>能接受「filter 範圍 = subset」這個語意縮小、但要使用者知道</li>
</ul>
<h3 id="不用">不用</h3>
<ul>
<li>Source 一次給完整 dataset（沒有 subset、不需要縮小）</li>
<li>使用者預期 filter 是「全集」、無法接受縮小</li>
<li>應用情境影響重大決策（finance、medical 等不能接受 silent 範圍縮小）</li>
</ul>
<hr>
<h2 id="跟策略-d誠實-ux的差別">跟策略 D（誠實 UX）的差別</h2>
<p>D 跟 E 都是「在 subset 上 filter」、差別在「怎麼告訴使用者」：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>D（誠實 UX）</th>
          <th>E（明示語意縮小）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範圍訊號</td>
          <td>即時數字（已掃 N / 命中 K）</td>
          <td>文字描述（一次性告知）</td>
      </tr>
      <tr>
          <td>UI 顯眼度</td>
          <td>高 — 每次都看得到</td>
          <td>低 — 看一次就過</td>
      </tr>
      <tr>
          <td>工程量</td>
          <td>中 — 要實作三數字</td>
          <td>低 — 改文字 / 加 docstring</td>
      </tr>
      <tr>
          <td>使用者參與</td>
          <td>點「再掃一批」續抓</td>
          <td>不續抓、自己判斷要不要 load more</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>filter 是主要互動模式</td>
          <td>filter 是次要功能、原型期</td>
      </tr>
  </tbody>
</table>
<p>簡言之：D 是「持續顯示掃描範圍」、E 是「告訴一次、之後不再提」。</p>
<hr>
<h2 id="明示的具體做法">「明示」的具體做法</h2>
<h3 id="ui-明示">UI 明示</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">type</span><span class="o">=</span><span class="s">&#34;search&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;Filter loaded results...&#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">small</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;hint&#34;</span><span class="p">&gt;</span>只在已載入的結果裡篩選。要看更多請先載入更多。<span class="p">&lt;/</span><span class="nt">small</span><span class="p">&gt;</span></span></span></code></pre></div><p>「Filter loaded results」、「已載入的結果裡」、「載入更多」 — 三個 cue 讓使用者知道範圍。</p>
<h3 id="api-contract-明示">API contract 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cm"> * Filter loaded results by predicate.
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="cm"> * NOTE: Operates on currently loaded subset only.
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="cm"> * Does NOT trigger fetch of un-loaded items. To filter the full
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="cm"> * dataset, use {@link searchAll} instead.
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="kd">function</span> <span class="nx">filterLoaded</span><span class="p">(</span><span class="nx">predicate</span><span class="o">:</span> <span class="p">(</span><span class="nx">item</span>: <span class="kt">Item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">boolean</span><span class="p">)</span><span class="o">:</span> <span class="nx">Item</span><span class="p">[];</span></span></span></code></pre></div><p>JSDoc / TSDoc 把語意寫進 API、IDE 提示能看到。</p>
<h3 id="docstring--readme-明示">Docstring / README 明示</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## Filter behavior
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sb">`filter()`</span> only operates on results currently loaded in client.
</span></span><span class="line"><span class="ln">4</span><span class="cl">If the source uses pagination, items not yet loaded are NOT included.
</span></span><span class="line"><span class="ln">5</span><span class="cl">For full-dataset filtering, the source must support server-side filter.</span></span></code></pre></div><p>文件級的明示、給開發者讀。</p>
<hr>
<h2 id="反例">反例</h2>
<h3 id="反例-1silent-縮小不告訴">反例 1：Silent 縮小（不告訴）</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">type</span><span class="o">=</span><span class="s">&#34;search&#34;</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">&#34;Filter results...&#34;</span><span class="p">&gt;</span></span></span></code></pre></div><p>「Filter results」沒指明「only loaded」 — 使用者預設是全集 filter、實際是 subset → 撞回 #55 <a href="../view-layer-filter-vs-source-layer/">層錯位</a> 的語意縫。</p>
<h3 id="反例-2明示位置使用者看不到">反例 2：明示位置使用者看不到</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ts" data-lang="ts"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cm"> * Filter results.
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="cm"> * Note: subset only.
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="cm"> */</span></span></span></code></pre></div><p>使用者只看 UI、不讀 docstring — 「明示」要在使用者會看到的位置（UI hint、tooltip、行為描述）。</p>
<h3 id="反例-3明示但不清楚">反例 3：明示但不清楚</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">small</span><span class="p">&gt;</span>限定範圍篩選<span class="p">&lt;/</span><span class="nt">small</span><span class="p">&gt;</span></span></span></code></pre></div><p>「限定範圍」太抽象、沒說明是什麼範圍。要寫具體：「已載入的 N 筆內」「不包含尚未載入的」。</p>
<hr>
<h2 id="何時-e-升級到-d">何時 E 升級到 D</h2>
<p>當以下任一觸發、把 E 升級到 D（誠實 UX 三數字）：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者依然誤以為是全集 filter</td>
          <td>升 D — 文字明示不夠</td>
      </tr>
      <tr>
          <td>Filter 後 0 筆的情境變常見</td>
          <td>升 D — 三數字能 disambiguate</td>
      </tr>
      <tr>
          <td>Filter 變主要互動模式（不再是次要功能）</td>
          <td>升 D — 顯眼度需要拉高</td>
      </tr>
      <tr>
          <td>Match 密度高、續抓 ROI 變正</td>
          <td>升 B（自動續抓）</td>
      </tr>
  </tbody>
</table>
<p>E 是「成本低的退路」、不是長期解。當需求成熟、應該升級到 D / A / C。</p>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 Pattern 的關係</h2>
<ul>
<li>E 是策略順序 A → C → B → D 之外的「最後退路」</li>
<li>E 跟 D 都是「在 subset 上做」、差別在告知方式</li>
<li>E 跟 #55 silent 反模式的差別：<strong>E 是 explicit 縮小、silent 是 implicit 縮小</strong></li>
</ul>
<p>選擇順序（重申）：<strong>A 推進 → C 多 index → B 自動續抓 → D 誠實 UX → E 明示縮小 → silent（反模式）</strong></p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source 不支援、工程量做不了 D</td>
          <td>用本 pattern</td>
      </tr>
      <tr>
          <td>Filter 行為已決定是 subset、但 UI 沒寫</td>
          <td>補 UI hint</td>
      </tr>
      <tr>
          <td>API 沒 docstring 說明 filter 範圍</td>
          <td>補 docstring</td>
      </tr>
      <tr>
          <td>使用者反映「filter 結果跟我想的不一樣」</td>
          <td>E 沒成功、升級到 D 或 A</td>
      </tr>
      <tr>
          <td>內心 OS：「反正 subset 就是 subset、寫了也沒人看」</td>
          <td>停 — silent 縮小是 #55 反模式</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：能接受語意縮小是可以、但必須明示。Silent 縮小（沒告知就 subset）等於 #55 層錯位、是反模式。E 的價值在「明示」這個動作、不在「subset」這個事實。</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>驗收的時間軸：四個 checkpoint</title><link>https://tarrragon.github.io/blog/report/verification-timeline-checkpoints/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/verification-timeline-checkpoints/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;blockquote>
&lt;p>驗收不是單一動作、是分散在四個時點的累積判斷。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Checkpoint&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>漏掉的 case、誤解的需求&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>Ship 前&lt;/td>
 &lt;td>E2E 跑邊界 / 規模 / 失敗 case&lt;/td>
 &lt;td>跨 case 整合錯、規模相依失敗、競態&lt;/td>
 &lt;td>高 — 設計 case&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ship 後&lt;/td>
 &lt;td>真實使用者紀錄、log monitor&lt;/td>
 &lt;td>silent 缺口、長尾 case、罕見組合&lt;/td>
 &lt;td>最高 — 反應慢&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 checkpoint 抓的失敗類型不同、跳過任一個 = 那類失敗會在更晚的 checkpoint 出現（或不出現、變成 silent bug）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼分散驗收而不是集中">為什麼分散驗收、而不是集中&lt;/h2>
&lt;h3 id="集中驗收的問題">集中驗收的問題&lt;/h3>
&lt;p>「寫完一次驗收完整」這個想法看似省事、實際撞兩個牆：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>失敗類型不在同一時點&lt;/strong>：開發中發現的是邏輯 bug、ship 前發現的是整合 bug、ship 後發現的是 silent 缺口 — 用同一種驗收方法不能 catch 全部&lt;/li>
&lt;li>&lt;strong>成本指數爆炸&lt;/strong>：到 ship 前才發現「需求理解錯」要重做整個 feature；到 ship 後才發現邏輯 bug 要熱修。早期 checkpoint 修一個 case 用 5 分鐘、ship 後修同個 case 用 5 小時&lt;/li>
&lt;/ol>
&lt;p>分散驗收 = 在每個 checkpoint catch 「該時點獨有的失敗類型」、累積成完整覆蓋。&lt;/p>
&lt;h3 id="早期-checkpoint-的槓桿">早期 checkpoint 的槓桿&lt;/h3>
&lt;p>「寫之前」的成本最低（列清單 5 分鐘）但能 catch 最貴的失敗類型（需求理解錯 = 整個 feature 重做）。&lt;strong>ROI 最高&lt;/strong>。&lt;/p>
&lt;p>「Ship 後」的成本最高（使用者反映、需要熱修）但只能 catch 最罕見的失敗類型。ROI 最低。&lt;/p>
&lt;p>實務上常常 collapse 成「寫的時候 + ship 後出問題才修」、跳過寫之前 / ship 前。這是把 ROI 倒過來。&lt;/p>
&lt;hr>
&lt;h2 id="四個-checkpoint-各自驗收什麼">四個 Checkpoint 各自驗收什麼&lt;/h2>
&lt;h3 id="checkpoint-1寫之前">Checkpoint 1：寫之前&lt;/h3>
&lt;p>&lt;strong>動作&lt;/strong>：列「使用者意圖完整集合」 — happy path、邊界 case、失敗 case、規模 case 各列幾條。&lt;/p>
&lt;p>&lt;strong>能 catch&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>需求理解跟使用者意圖不同&lt;/li>
&lt;li>邊界 case 從一開始就忘了想&lt;/li>
&lt;li>規模 case 沒考慮（10 筆 vs 10 萬筆行為不同）&lt;/li>
&lt;li>隱含假設沒攤開（「應該都會有 title」「永遠不會空」）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>範例&lt;/strong>：寫 filter 之前列：「title 含 X、content 含 X、兩者都含、都不含、source 全空、source 全是、稀疏 case、密集 case」 — 8 個 case 寫之前看見、實作時主動處理。&lt;/p>
&lt;h3 id="checkpoint-2開發中">Checkpoint 2：開發中&lt;/h3>
&lt;p>&lt;strong>動作&lt;/strong>：寫一塊測一塊 — 單元跑通、視覺看一眼、邊改邊試。&lt;/p>
&lt;p>&lt;strong>能 catch&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>邏輯錯誤（branch 寫錯、迴圈邊界錯）&lt;/li>
&lt;li>視覺錯誤（layout 跑掉、樣式套錯）&lt;/li>
&lt;li>API 用錯（呼叫順序錯、參數錯）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>不能 catch&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>跨多個 case 的整合錯&lt;/li>
&lt;li>規模相依失敗&lt;/li>
&lt;li>競態 / async race&lt;/li>
&lt;li>跨環境差異&lt;/li>
&lt;/ul>
&lt;h3 id="checkpoint-3ship-前">Checkpoint 3：Ship 前&lt;/h3>
&lt;p>&lt;strong>動作&lt;/strong>：E2E 跑邊界 / 規模 / 失敗 case。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<blockquote>
<p>驗收不是單一動作、是分散在四個時點的累積判斷。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>Checkpoint</th>
          <th>時點</th>
          <th>能驗收的失敗類型</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫之前</td>
          <td>開工前列「使用者意圖完整集」</td>
          <td>漏掉的 case、誤解的需求</td>
          <td>低 — 列清單</td>
      </tr>
      <tr>
          <td>開發中</td>
          <td>寫一塊測一塊</td>
          <td>邏輯錯誤、視覺錯誤、單元失敗</td>
          <td>中 — 小範圍</td>
      </tr>
      <tr>
          <td>Ship 前</td>
          <td>E2E 跑邊界 / 規模 / 失敗 case</td>
          <td>跨 case 整合錯、規模相依失敗、競態</td>
          <td>高 — 設計 case</td>
      </tr>
      <tr>
          <td>Ship 後</td>
          <td>真實使用者紀錄、log monitor</td>
          <td>silent 缺口、長尾 case、罕見組合</td>
          <td>最高 — 反應慢</td>
      </tr>
  </tbody>
</table>
<p>每個 checkpoint 抓的失敗類型不同、跳過任一個 = 那類失敗會在更晚的 checkpoint 出現（或不出現、變成 silent bug）。</p>
<hr>
<h2 id="為什麼分散驗收而不是集中">為什麼分散驗收、而不是集中</h2>
<h3 id="集中驗收的問題">集中驗收的問題</h3>
<p>「寫完一次驗收完整」這個想法看似省事、實際撞兩個牆：</p>
<ol>
<li><strong>失敗類型不在同一時點</strong>：開發中發現的是邏輯 bug、ship 前發現的是整合 bug、ship 後發現的是 silent 缺口 — 用同一種驗收方法不能 catch 全部</li>
<li><strong>成本指數爆炸</strong>：到 ship 前才發現「需求理解錯」要重做整個 feature；到 ship 後才發現邏輯 bug 要熱修。早期 checkpoint 修一個 case 用 5 分鐘、ship 後修同個 case 用 5 小時</li>
</ol>
<p>分散驗收 = 在每個 checkpoint catch 「該時點獨有的失敗類型」、累積成完整覆蓋。</p>
<h3 id="早期-checkpoint-的槓桿">早期 checkpoint 的槓桿</h3>
<p>「寫之前」的成本最低（列清單 5 分鐘）但能 catch 最貴的失敗類型（需求理解錯 = 整個 feature 重做）。<strong>ROI 最高</strong>。</p>
<p>「Ship 後」的成本最高（使用者反映、需要熱修）但只能 catch 最罕見的失敗類型。ROI 最低。</p>
<p>實務上常常 collapse 成「寫的時候 + ship 後出問題才修」、跳過寫之前 / ship 前。這是把 ROI 倒過來。</p>
<hr>
<h2 id="四個-checkpoint-各自驗收什麼">四個 Checkpoint 各自驗收什麼</h2>
<h3 id="checkpoint-1寫之前">Checkpoint 1：寫之前</h3>
<p><strong>動作</strong>：列「使用者意圖完整集合」 — happy path、邊界 case、失敗 case、規模 case 各列幾條。</p>
<p><strong>能 catch</strong>：</p>
<ul>
<li>需求理解跟使用者意圖不同</li>
<li>邊界 case 從一開始就忘了想</li>
<li>規模 case 沒考慮（10 筆 vs 10 萬筆行為不同）</li>
<li>隱含假設沒攤開（「應該都會有 title」「永遠不會空」）</li>
</ul>
<p><strong>範例</strong>：寫 filter 之前列：「title 含 X、content 含 X、兩者都含、都不含、source 全空、source 全是、稀疏 case、密集 case」 — 8 個 case 寫之前看見、實作時主動處理。</p>
<h3 id="checkpoint-2開發中">Checkpoint 2：開發中</h3>
<p><strong>動作</strong>：寫一塊測一塊 — 單元跑通、視覺看一眼、邊改邊試。</p>
<p><strong>能 catch</strong>：</p>
<ul>
<li>邏輯錯誤（branch 寫錯、迴圈邊界錯）</li>
<li>視覺錯誤（layout 跑掉、樣式套錯）</li>
<li>API 用錯（呼叫順序錯、參數錯）</li>
</ul>
<p><strong>不能 catch</strong>：</p>
<ul>
<li>跨多個 case 的整合錯</li>
<li>規模相依失敗</li>
<li>競態 / async race</li>
<li>跨環境差異</li>
</ul>
<h3 id="checkpoint-3ship-前">Checkpoint 3：Ship 前</h3>
<p><strong>動作</strong>：E2E 跑邊界 / 規模 / 失敗 case。</p>
<p><strong>能 catch</strong>：</p>
<ul>
<li>跨 case 整合錯（filter 切換 + load more 互動）</li>
<li>規模相依（500 筆時 jank）</li>
<li>競態（快速切換 query 時）</li>
<li>真實環境 case（slow network、large data）</li>
</ul>
<p><strong>不能 catch</strong>：</p>
<ul>
<li>罕見組合（特定 user pattern）</li>
<li>真實使用者意外行為</li>
<li>長尾邊界（千分之一機率的狀態）</li>
</ul>
<p><strong>這個 checkpoint 最常被跳過</strong> — 因為設計 E2E case 成本高、要刻意製造規模 / 失敗 / 競態場景。但跳過 = ship 後才發現。</p>
<h3 id="checkpoint-4ship-後">Checkpoint 4：Ship 後</h3>
<p><strong>動作</strong>：log monitor、error tracking、使用者行為紀錄。</p>
<p><strong>能 catch</strong>：</p>
<ul>
<li>silent 缺口（沒人 report、log 看出來）</li>
<li>罕見組合</li>
<li>真實使用者意外行為</li>
<li>跨時間退化（穩定 vs 漸變）</li>
</ul>
<p><strong>特性</strong>：成本最高、反應最慢、只能 catch 前三個 checkpoint 都漏的失敗。<strong>價值在於「保底」、不是主力驗收</strong>。</p>
<hr>
<h2 id="為什麼-ship-前-checkpoint-最常被跳過">為什麼 Ship 前 checkpoint 最常被跳過</h2>
<p>四個 checkpoint 中、Ship 前是被跳過機率最高的一個。原因是結構性的、不是隨機的：</p>
<table>
  <thead>
      <tr>
          <th>Checkpoint</th>
          <th>觸發機制</th>
          <th>是否有便利路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫之前</td>
          <td>外部驅動（需求 / spec）</td>
          <td>有 — 別人推著走</td>
      </tr>
      <tr>
          <td>開發中</td>
          <td>內建在寫的動作裡</td>
          <td>有 — 寫一塊看一眼是反射動作</td>
      </tr>
      <tr>
          <td><strong>Ship 前</strong></td>
          <td><strong>要主動設計 case</strong></td>
          <td><strong>沒有 — 需要刻意停下來想邊界</strong></td>
      </tr>
      <tr>
          <td>Ship 後</td>
          <td>被動（使用者反映）</td>
          <td>有 — 別人推著走</td>
      </tr>
  </tbody>
</table>
<p>寫之前跟 Ship 後都是「被外部 / 別人推著」、有現成觸發；開發中是反射動作、不需要刻意。<strong>只有 Ship 前需要寫的人主動停下、設計 E2E case、執行 case</strong> — 沒有現成觸發、沒有便利路徑。</p>
<p>這正是 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a> 在驗收動作上的應用：跟「便利路徑」對齊的 checkpoint 會被做、要「主動設計」的 checkpoint 會被跳。</p>
<p>修這個結構性偏差的方法：</p>
<ul>
<li>把 Ship 前 case 設計列進開工前的「使用者意圖完整集合」（推到 Checkpoint 1、有便利路徑）</li>
<li>用 layout test / E2E test 把 case 固化（<a href="../layout-tests-with-playwright/">#15</a>）— 寫一次、之後 CI 自動跑、不需要主動觸發</li>
<li>公司 / 團隊建立「Ship 前 checkpoint review」會議 — 把它變成外部驅動</li>
</ul>
<hr>
<h2 id="為什麼-checkpoint-1寫之前也常被跳過--同個結構性偏差">為什麼 Checkpoint 1（寫之前）也常被跳過 — 同個結構性偏差</h2>
<p>Checkpoint 1 跟 Ship 前 checkpoint 共享同一個結構性問題：<strong>沒有便利路徑、需要刻意停下來</strong>。</p>
<table>
  <thead>
      <tr>
          <th>Checkpoint</th>
          <th>該做的事</th>
          <th>為什麼會被跳過</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫之前</td>
          <td>列「使用者意圖完整集合」</td>
          <td>沒既有觸發、要刻意停 5 分鐘想</td>
      </tr>
      <tr>
          <td>Ship 前</td>
          <td>設計 E2E case + 執行</td>
          <td>沒既有觸發、要刻意設計</td>
      </tr>
  </tbody>
</table>
<p><strong>真實案例（這個 blog 的 search filter bug 修復）</strong>：</p>
<p>修 #55 層錯位 bug 時、跳過了 Checkpoint 1。直接從 bug 描述進策略選擇 + 實作。Phase 1-4 都做完、跑了 Playwright tests 過 4/4 — 看起來完工。</p>
<p>事後 retrospective Checkpoint 1（user 提醒「需求確認是該 skill 最重要功能之一」）才發現遺漏：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Checkpoint 1 漏掉的 case</th>
          <th>跑驗證才發現</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>URL state</td>
          <td><code>?q=X&amp;scope=Y</code> 持久化</td>
          <td>既有實作完全沒處理 URL state（<a href="../url-as-state-container/">#70</a>）</td>
      </tr>
      <tr>
          <td>A11y</td>
          <td>Tab order 跟 mental model 對齊</td>
          <td>scope 在 search input 之前、反 mental model（<a href="../tab-order-mental-model-alignment/">#71</a>）</td>
      </tr>
      <tr>
          <td>Filter UX</td>
          <td>Type/tag filter 在 sub-mode 完全消失</td>
          <td>Silent 限制、使用者可能誤以為 bug</td>
      </tr>
  </tbody>
</table>
<p>修完 bug + ship test = 表面完成。但 Checkpoint 1 本來該 catch 的 3 個 case 都漏到後期 retrospective 才被發現。<strong>Test 過 ≠ 對齊使用者完整意圖</strong>。</p>
<p>修這個結構性偏差的方法（同 Ship 前）：</p>
<ul>
<li>把「列使用者意圖完整集」做成 checklist 模板、寫之前 5 分鐘填、外化成觸發</li>
<li>用 <a href="../decide-vs-confirm-boundary/">#21 visible 三問</a> 強迫自己列出「使用者會看到的維度」</li>
<li>修 bug 不止修 bug、也檢視該 feature 的所有相關意圖維度</li>
</ul>
<p><a href="../test-first-red-before-green/">#69 Test-First</a> 是 Checkpoint 2/3 的具體協議；本卡是 Checkpoint 1 + 為什麼前後兩個 checkpoint 都被結構性跳過的解釋。</p>
<p>更上位的「為什麼跳過」解釋見 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發的工作會被結構性跳過</a> — 本卡的 Checkpoint 1 + Ship 前是 #72 在「驗收動作」面向的展現、修法（外化觸發到 PR template / CI / pair）對應 #72 的 L3-L5 對策。</p>
<hr>
<h2 id="瀑布原則漏一層代價指數放大">瀑布原則：漏一層代價指數放大</h2>
<p>漏掉一個 checkpoint 不是線性影響、是指數放大：</p>
<table>
  <thead>
      <tr>
          <th>漏掉哪個 checkpoint</th>
          <th>該失敗會在哪 checkpoint 才被發現</th>
          <th>修復成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫之前</td>
          <td>Ship 前（甚至 ship 後）</td>
          <td>重做整個 feature（×100）</td>
      </tr>
      <tr>
          <td>開發中</td>
          <td>Ship 前</td>
          <td>改一個 module（×10）</td>
      </tr>
      <tr>
          <td>Ship 前</td>
          <td>Ship 後</td>
          <td>熱修 + 信任損失（×100）</td>
      </tr>
      <tr>
          <td>Ship 後</td>
          <td>永遠不修</td>
          <td>累積技術債（不可估）</td>
      </tr>
  </tbody>
</table>
<p>「Ship 後修 bug 多」不是「ship 後驗收做得好」、是「上游 checkpoint 沒做好把 bug 全推下來」 — 看起來在做事、實際在付出指數成本。</p>
<h3 id="為什麼指數放大">為什麼指數放大</h3>
<p>每個 checkpoint 漏掉的失敗、進入下一個 checkpoint 時：</p>
<ol>
<li><strong>Context 已經消失</strong>：下一個 checkpoint 才發現時、寫的人可能已經在做其他事、要重建上下文</li>
<li><strong>依賴已經建立</strong>：別的代碼已經依賴這個有 bug 的 feature、改一處要連帶改五處</li>
<li><strong>使用者已經受影響</strong>：ship 後修還要處理使用者信任 / 資料一致性 / 通知</li>
</ol>
<p>每多漏一層、上述三個因素都疊加、成本翻 N 倍而不是 +N。</p>
<h3 id="防線概念每個-checkpoint-是獨立防線">防線概念：每個 checkpoint 是獨立防線</h3>
<p>把驗收看成 <strong>defense in depth</strong> — 每個 checkpoint 是一道防線、漏掉一道下一道接住。但每道防線的修復成本不同、越上游越便宜。</p>
<p>跟 a11y 三道防線（<a href="../focus-management-on-dom-move/">#37 動態 focus</a> / <a href="../aria-live-for-dynamic-content/">#38 aria-live</a> / <a href="../native-html-over-aria-role/">#39 native HTML</a>）共骨：分散獨立防線比集中單一防線更穩、因為單點失效不會打穿全系統。</p>
<hr>
<h2 id="checkpoint-之間的累積關係">Checkpoint 之間的累積關係</h2>
<p>每個 checkpoint 都該補前面的洞 — 不是等量分配、是優先填上游：</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">[寫之前 ROI: 高]   抓需求 / 邊界 / 規模意圖
</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">[開發中 ROI: 中]   抓邏輯 / 視覺 / 單元
</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">[Ship 前 ROI: 中-低] 抓整合 / 規模 / 競態
</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">[Ship 後 ROI: 低]   抓罕見 / silent / 長尾</span></span></code></pre></div><p>「Ship 後修 bug 多」= 上游 checkpoint 沒做好、不是「ship 後驗收做得好」。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<h3 id="跟-42-2-次門檻">跟 <a href="../two-occurrence-threshold/">#42 2 次門檻</a></h3>
<p>「畫面對一次」「測試過一次」「使用者沒反映一次」都是低資訊量訊號 — 對應「開發中 checkpoint 過了一次」。第 2 次（跨多個 case / 規模 / 時間）才是真訊號 — 對應「ship 前 checkpoint 也過了」。</p>
<p><a href="../visual-completion-vs-functional-completion/">#56 視覺完成 ≠ 功能完成</a> 是這個關係在「視覺驗收」面向的應用。</p>
<h3 id="跟-67-寫作便利度跟意圖對齊反相關">跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></h3>
<p>寫之前 checkpoint 列「意圖完整集」 = 跟便利度脫鉤、強制看見意圖。跳過 = 接受被便利驅動。</p>
<h3 id="跟-56-視覺完成--功能完成">跟 <a href="../visual-completion-vs-functional-completion/">#56 視覺完成 ≠ 功能完成</a></h3>
<p>「畫面對」是開發中 checkpoint 的訊號、不是終點訊號。把它當完工 = 跳過 ship 前 / ship 後 checkpoint。</p>
<hr>
<h2 id="不該套用本原則的情境">不該套用本原則的情境</h2>
<p>「驗收分散在四個時點」這條原則在 ship 給其他人的開發情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不該套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純 research / 實驗</td>
          <td>不會 ship 給別人、ship 前 / ship 後 checkpoint 都不存在</td>
      </tr>
      <tr>
          <td>一次性 script</td>
          <td>跑完就丟、沒有「ship」這個階段、四 checkpoint 概念不適用</td>
      </tr>
      <tr>
          <td>純 prototype</td>
          <td>預期會被丟掉、ship 後 monitor 沒意義、開發中 checkpoint 夠</td>
      </tr>
      <tr>
          <td>個人玩具專案</td>
          <td>失敗只影響自己、信任損失成本 ≈ 0、可放寬</td>
      </tr>
  </tbody>
</table>
<p>四類共同特徵：<strong>「ship 後的失敗成本」≈ 0</strong> — 因為沒有真實使用者、沒有信任損失、沒有累積技術債。本原則的瀑布原則建立在「漏一層代價指數放大」上、ship 後成本為 0 時自然不放大。</p>
<p>判讀：寫之前自問「失敗會不會影響別人」 — 否 → 本原則可放寬；是 → 本原則嚴格適用。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫之前沒列「使用者意圖完整集合」</td>
          <td>補 — 5 分鐘列、可以避免 5 小時重做</td>
      </tr>
      <tr>
          <td>開發中只測了 happy path</td>
          <td>補邊界 / 失敗 / 規模 case</td>
      </tr>
      <tr>
          <td>Ship 前沒設計 E2E case、預設「能 build 就 OK」</td>
          <td>加：規模 case + 競態 case + 失敗 case</td>
      </tr>
      <tr>
          <td>Ship 後沒 log / monitor</td>
          <td>加 — 保底 checkpoint 沒設 = 永遠不知道有 silent bug</td>
      </tr>
      <tr>
          <td>Bug report 含「ship 後一週才被發現」</td>
          <td>表示前三個 checkpoint 漏了、要回頭加固</td>
      </tr>
      <tr>
          <td>內心 OS：「之後 QA / 使用者會發現」</td>
          <td>是「集中驗收」幻覺、跳過早期 checkpoint</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：驗收的價值在「分散在多個時點」、每個 checkpoint catch 不同類型的失敗。把驗收 collapse 成單一時點 = 接受該時點之外的失敗都 silent 通過。早期 checkpoint ROI 最高、跳過代價最大。</p>
<p>Checkpoint 2「開發中」+ Checkpoint 3「Ship 前」內部的具體協議：<a href="../test-first-red-before-green/">#69 Test-First：先看到 RED 才相信 GREEN</a> — 寫測試 + 跑兩次（RED-buggy + GREEN-fixed）才能驗證測試本身有用。跳過 RED = 接受測試可能是壞的。</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/search-engine-matching-mode-mismatch/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/search-engine-matching-mode-mismatch/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>搜尋引擎的「匹配模式」是個經常被忽略的維度&lt;/strong> — 工具的預設行為跟使用者的 mental model 不對齊時、產生 silent 失敗：使用者打字、看不到預期結果、誤以為「沒有」、不會 report bug。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>匹配模式&lt;/th>
 &lt;th>例：query「pre」會匹配&lt;/th>
 &lt;th>典型來源&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Exact&lt;/td>
 &lt;td>&lt;code>pre&lt;/code>（不含「pre」這個 token）&lt;/td>
 &lt;td>DB &lt;code>=&lt;/code> 比較&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prefix&lt;/td>
 &lt;td>&lt;code>pre&lt;/code>、&lt;code>prefix&lt;/code>、&lt;code>prefetch&lt;/code>、&lt;code>presence&lt;/code>&lt;/td>
 &lt;td>Pagefind / Lunr 預設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Substring&lt;/td>
 &lt;td>上面 + &lt;code>backpressure&lt;/code>、&lt;code>SuperPress&lt;/code>&lt;/td>
 &lt;td>DB &lt;code>LIKE '%pre%'&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fuzzy&lt;/td>
 &lt;td>上面 + &lt;code>prv&lt;/code>、&lt;code>pre1&lt;/code>（編輯距離）&lt;/td>
 &lt;td>Algolia、TypeSense&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Semantic&lt;/td>
 &lt;td>上面 + &lt;code>before&lt;/code>、&lt;code>prior&lt;/code>（語意相近）&lt;/td>
 &lt;td>Vector search / LLM&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>使用者被 Google / 桌面搜尋訓練、預期 &lt;strong>substring 或更高層級&lt;/strong>。預設拿到 prefix 的 site search → 「pre」找不到 backpressure → 看起來像 bug 但其實是 capability 落差。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼預設是-prefix">為什麼預設是 prefix&lt;/h2>
&lt;p>Static site search engines（Pagefind / Lunr / MiniSearch 預設）選 prefix matching 的原因：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>因素&lt;/th>
 &lt;th>Prefix&lt;/th>
 &lt;th>Substring&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Index size&lt;/td>
 &lt;td>O(N)&lt;/td>
 &lt;td>O(N²)（要 index 所有後綴）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query speed&lt;/td>
 &lt;td>快（trie）&lt;/td>
 &lt;td>慢（全掃 substring）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨語言支援&lt;/td>
 &lt;td>容易&lt;/td>
 &lt;td>中文 / CJK 邊界不明確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build time&lt;/td>
 &lt;td>快&lt;/td>
 &lt;td>慢&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對 static site（沒 server）、index 是要下載到 client 的 — substring index 可能 5-10x 大、unacceptable。Pagefind / Lunr 選 prefix 是「對齊 size constraint」、不是「對齊使用者意圖」。&lt;/p>
&lt;p>這是個典型的 &lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a> — 工具預設是「實作便利位置」、不是「使用者意圖位置」。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼這個-gap-是-silent">為什麼這個 gap 是 silent&lt;/h2>
&lt;p>跟 &lt;a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位&lt;/a> 共用結構：使用者打字看到結果列表、結果不空、看起來「有東西」、不會懷疑 engine 沒在做完整 search。&lt;/p>
&lt;p>silent 失敗的條件：&lt;/p>
&lt;ol>
&lt;li>Prefix matching 對某些 query 仍能回到結果（排版上看起來「有用」）&lt;/li>
&lt;li>使用者不知道「沒看到的還有什麼」&lt;/li>
&lt;li>只有當 query 剛好不是任何 token 的 prefix、才會 0 結果（極少見、這時才會懷疑）&lt;/li>
&lt;/ol>
&lt;p>對照 &lt;a href="../view-layer-filter-vs-source-layer/">#55 silent 失敗條件&lt;/a> 三條件 — 完全一樣的結構：「有部分結果掩蓋了缺口」。&lt;/p>
&lt;hr>
&lt;h2 id="多面向跨工具的匹配模式對照">多面向：跨工具的匹配模式對照&lt;/h2>
&lt;h3 id="前端-client-side-search">前端 client-side search&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>預設匹配模式&lt;/th>
 &lt;th>可調整為&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Pagefind v1.5&lt;/td>
 &lt;td>Word-prefix&lt;/td>
 &lt;td>Exact only（&lt;code>useExact&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lunr&lt;/td>
 &lt;td>Stem + prefix&lt;/td>
 &lt;td>Wildcard（&lt;code>q+'*'&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MiniSearch&lt;/td>
 &lt;td>Prefix&lt;/td>
 &lt;td>Substring（&lt;code>prefix: false, fuzzy: 0.2&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>FlexSearch&lt;/td>
 &lt;td>Token-based&lt;/td>
 &lt;td>多種 tokenizer（含 ngram）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fuse.js&lt;/td>
 &lt;td>Fuzzy&lt;/td>
 &lt;td>可關掉 fuzzy 變 substring&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="backend--db">Backend / DB&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>SQL &lt;code>=&lt;/code>&lt;/td>
 &lt;td>Exact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SQL &lt;code>LIKE '%X%'&lt;/code>&lt;/td>
 &lt;td>Substring（O(n) scan）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>SQL FULLTEXT&lt;/td>
 &lt;td>Token + stem + (有時 prefix)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ElasticSearch&lt;/td>
 &lt;td>配置：term / match / wildcard / fuzzy / regexp&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PostgreSQL trigram&lt;/td>
 &lt;td>Substring + similarity&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vector DB（Pinecone 等）&lt;/td>
 &lt;td>Semantic&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="命令列--ide-搜尋">命令列 / IDE 搜尋&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;code>grep&lt;/code>&lt;/td>
 &lt;td>Substring（regex）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>rg&lt;/code>&lt;/td>
 &lt;td>Substring（smart-case + regex）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vim &lt;code>/&lt;/code>&lt;/td>
 &lt;td>Regex&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VSCode 搜尋&lt;/td>
 &lt;td>Substring（含 fuzzy file search）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>共通結構&lt;/strong>：每個工具預設不同、使用者帶著舊工具的 expectation 來、不對齊時 silent 失敗。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>搜尋引擎的「匹配模式」是個經常被忽略的維度</strong> — 工具的預設行為跟使用者的 mental model 不對齊時、產生 silent 失敗：使用者打字、看不到預期結果、誤以為「沒有」、不會 report bug。</p>
<table>
  <thead>
      <tr>
          <th>匹配模式</th>
          <th>例：query「pre」會匹配</th>
          <th>典型來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Exact</td>
          <td><code>pre</code>（不含「pre」這個 token）</td>
          <td>DB <code>=</code> 比較</td>
      </tr>
      <tr>
          <td>Prefix</td>
          <td><code>pre</code>、<code>prefix</code>、<code>prefetch</code>、<code>presence</code></td>
          <td>Pagefind / Lunr 預設</td>
      </tr>
      <tr>
          <td>Substring</td>
          <td>上面 + <code>backpressure</code>、<code>SuperPress</code></td>
          <td>DB <code>LIKE '%pre%'</code></td>
      </tr>
      <tr>
          <td>Fuzzy</td>
          <td>上面 + <code>prv</code>、<code>pre1</code>（編輯距離）</td>
          <td>Algolia、TypeSense</td>
      </tr>
      <tr>
          <td>Semantic</td>
          <td>上面 + <code>before</code>、<code>prior</code>（語意相近）</td>
          <td>Vector search / LLM</td>
      </tr>
  </tbody>
</table>
<p>使用者被 Google / 桌面搜尋訓練、預期 <strong>substring 或更高層級</strong>。預設拿到 prefix 的 site search → 「pre」找不到 backpressure → 看起來像 bug 但其實是 capability 落差。</p>
<hr>
<h2 id="為什麼預設是-prefix">為什麼預設是 prefix</h2>
<p>Static site search engines（Pagefind / Lunr / MiniSearch 預設）選 prefix matching 的原因：</p>
<table>
  <thead>
      <tr>
          <th>因素</th>
          <th>Prefix</th>
          <th>Substring</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Index size</td>
          <td>O(N)</td>
          <td>O(N²)（要 index 所有後綴）</td>
      </tr>
      <tr>
          <td>Query speed</td>
          <td>快（trie）</td>
          <td>慢（全掃 substring）</td>
      </tr>
      <tr>
          <td>跨語言支援</td>
          <td>容易</td>
          <td>中文 / CJK 邊界不明確</td>
      </tr>
      <tr>
          <td>Build time</td>
          <td>快</td>
          <td>慢</td>
      </tr>
  </tbody>
</table>
<p>對 static site（沒 server）、index 是要下載到 client 的 — substring index 可能 5-10x 大、unacceptable。Pagefind / Lunr 選 prefix 是「對齊 size constraint」、不是「對齊使用者意圖」。</p>
<p>這是個典型的 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a> — 工具預設是「實作便利位置」、不是「使用者意圖位置」。</p>
<hr>
<h2 id="為什麼這個-gap-是-silent">為什麼這個 gap 是 silent</h2>
<p>跟 <a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位</a> 共用結構：使用者打字看到結果列表、結果不空、看起來「有東西」、不會懷疑 engine 沒在做完整 search。</p>
<p>silent 失敗的條件：</p>
<ol>
<li>Prefix matching 對某些 query 仍能回到結果（排版上看起來「有用」）</li>
<li>使用者不知道「沒看到的還有什麼」</li>
<li>只有當 query 剛好不是任何 token 的 prefix、才會 0 結果（極少見、這時才會懷疑）</li>
</ol>
<p>對照 <a href="../view-layer-filter-vs-source-layer/">#55 silent 失敗條件</a> 三條件 — 完全一樣的結構：「有部分結果掩蓋了缺口」。</p>
<hr>
<h2 id="多面向跨工具的匹配模式對照">多面向：跨工具的匹配模式對照</h2>
<h3 id="前端-client-side-search">前端 client-side search</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>預設匹配模式</th>
          <th>可調整為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pagefind v1.5</td>
          <td>Word-prefix</td>
          <td>Exact only（<code>useExact</code>）</td>
      </tr>
      <tr>
          <td>Lunr</td>
          <td>Stem + prefix</td>
          <td>Wildcard（<code>q+'*'</code>）</td>
      </tr>
      <tr>
          <td>MiniSearch</td>
          <td>Prefix</td>
          <td>Substring（<code>prefix: false, fuzzy: 0.2</code>）</td>
      </tr>
      <tr>
          <td>FlexSearch</td>
          <td>Token-based</td>
          <td>多種 tokenizer（含 ngram）</td>
      </tr>
      <tr>
          <td>Fuse.js</td>
          <td>Fuzzy</td>
          <td>可關掉 fuzzy 變 substring</td>
      </tr>
  </tbody>
</table>
<h3 id="backend--db">Backend / DB</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>匹配模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQL <code>=</code></td>
          <td>Exact</td>
      </tr>
      <tr>
          <td>SQL <code>LIKE '%X%'</code></td>
          <td>Substring（O(n) scan）</td>
      </tr>
      <tr>
          <td>SQL FULLTEXT</td>
          <td>Token + stem + (有時 prefix)</td>
      </tr>
      <tr>
          <td>ElasticSearch</td>
          <td>配置：term / match / wildcard / fuzzy / regexp</td>
      </tr>
      <tr>
          <td>PostgreSQL trigram</td>
          <td>Substring + similarity</td>
      </tr>
      <tr>
          <td>Vector DB（Pinecone 等）</td>
          <td>Semantic</td>
      </tr>
  </tbody>
</table>
<h3 id="命令列--ide-搜尋">命令列 / IDE 搜尋</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>預設</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>grep</code></td>
          <td>Substring（regex）</td>
      </tr>
      <tr>
          <td><code>rg</code></td>
          <td>Substring（smart-case + regex）</td>
      </tr>
      <tr>
          <td>Vim <code>/</code></td>
          <td>Regex</td>
      </tr>
      <tr>
          <td>VSCode 搜尋</td>
          <td>Substring（含 fuzzy file search）</td>
      </tr>
  </tbody>
</table>
<p><strong>共通結構</strong>：每個工具預設不同、使用者帶著舊工具的 expectation 來、不對齊時 silent 失敗。</p>
<hr>
<h2 id="識別三問">識別三問</h2>
<p>寫之前 / debug 時、自問：</p>
<h3 id="1-這個工具的預設匹配模式是什麼">1. 這個工具的預設匹配模式是什麼？</h3>
<p>讀 docs、不要假設。Pagefind docs 寫 &ldquo;Pagefind matches by word prefix&rdquo;。Lunr 文件寫 &ldquo;Lunr does prefix matching by default&rdquo;。 預設不是直覺。</p>
<h3 id="2-使用者預期哪種匹配模式">2. 使用者預期哪種匹配模式？</h3>
<p>使用者被別的工具訓練。使用者基數越大、越接近 Google substring + fuzzy 預期。</p>
<table>
  <thead>
      <tr>
          <th>使用者類型</th>
          <th>預期匹配模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般使用者（被 Google 訓練）</td>
          <td>Substring + fuzzy + semantic</td>
      </tr>
      <tr>
          <td>開發者（用 grep / IDE）</td>
          <td>Substring + regex</td>
      </tr>
      <tr>
          <td>資料庫使用者（寫 SQL）</td>
          <td>看你給的 hint</td>
      </tr>
      <tr>
          <td>命令列重度使用者</td>
          <td>預設 regex</td>
      </tr>
  </tbody>
</table>
<h3 id="3-gap-多大是否-silent">3. Gap 多大？是否 silent？</h3>
<p>工具預設 vs 使用者預期不一致時、評估「使用者會在多少 case 中遇到不一致」。</p>
<ul>
<li>Prefix vs Substring：使用者只要打詞中間部分就 silent 失敗、頻率高</li>
<li>Prefix vs Fuzzy：使用者打錯字才會發現、頻率低</li>
<li>Substring vs Semantic：使用者用同義詞才會發現、頻率中</li>
</ul>
<p>頻率高的 gap 必須有對策。</p>
<hr>
<h2 id="五種對策跟-59-filter--source-五策略-同構">五種對策（跟 <a href="../filter-source-composition-strategies/">#59 Filter × Source 五策略</a> 同構）</h2>
<h3 id="a選用支援目標匹配模式的引擎">A：選用支援目標匹配模式的引擎</h3>
<p>Pagefind 不支援 substring → 換 MiniSearch / FlexSearch。Lunr 不支援 fuzzy → 換 FlexSearch / Fuse.js。</p>
<ul>
<li><strong>適合</strong>：早期決策、index size 不是 bottleneck、能接受工程量</li>
<li><strong>代價</strong>：換引擎成本（API 不同、index 重建、UI 重整合）</li>
</ul>
<h3 id="b在-build-time-pre-tokenize增加替代-token">B：在 build time pre-tokenize、增加替代 token</h3>
<p>在 build pipeline 拆字、把 <code>backpressure</code> 加進 search index 的多個 token：<code>back</code> + <code>pressure</code> + <code>backpressure</code> + <code>back-pressure</code>。Pagefind 透過 <code>data-pagefind-meta</code> 或多份 hidden text 注入。</p>
<ul>
<li><strong>適合</strong>：少量已知關鍵詞 / 跨語言邊界（中文）/ 能控 build pipeline</li>
<li><strong>代價</strong>：手動標記、index 變大、新詞要加進清單</li>
</ul>
<h3 id="cclient-side-fallback-substring-search">C：Client-side fallback substring search</h3>
<p>Pagefind 找不到時、fetch 一份頁面 metadata（title + slug）、做 client-side substring filter。</p>
<ul>
<li><strong>適合</strong>：頁面數量 &lt; 10000、可接受第二層延遲</li>
<li><strong>代價</strong>：需要額外 fetch + 客戶端 substring scan、兩種 result UI 整合</li>
</ul>
<h3 id="dux-hint-明示匹配模式">D：UX hint 明示匹配模式</h3>
<p>把限制告訴使用者：「搜尋為前綴匹配、想找 X 請打 Y」。對應 <a href="../pattern-explicit-semantic-narrowing/">#66 明示語意縮小</a>。</p>
<ul>
<li><strong>適合</strong>：成本最低、只需文字 hint</li>
<li><strong>代價</strong>：使用者要學新規則、不對齊 Google expectation</li>
</ul>
<h3 id="e接受限制不告知">E：接受限制（不告知）</h3>
<p>不做任何處理、silent 接受。這是反模式（同 <a href="../view-layer-filter-vs-source-layer/">#55 silent 失敗</a>）— 使用者誤以為「沒有相關內容」、放棄。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../view-layer-filter-vs-source-layer/">#55 Filter × Source 層錯位</a></td>
          <td>都是「使用者意圖跟工具實際行為的 silent gap」、本卡是 matching 維度的展現</td>
      </tr>
      <tr>
          <td><a href="../data-source-shape-defines-feature-shape/">#63 資料源的形狀</a></td>
          <td>形狀是 source 的 capability 維度、本卡是「matching mode」這個 capability 維度</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>工具預設是實作便利、使用者預期是 mental model 對齊、反相關</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a> Checkpoint 1</td>
          <td>「source capabilities 是否對齊使用者預期」屬意圖完整集 — 容易跳過</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a></td>
          <td>「讀 search engine docs 確認 matching mode」沒便利路徑、容易跳過</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="對應的實作篇">對應的實作篇</h2>
<ul>
<li>本 blog 搜尋頁的 Pagefind prefix-match 限制（這次 user 報的 case）</li>
<li>任何用 client-side search 的 SPA / 靜態站</li>
<li>內部 admin tool 的 search box（往往用 SQL <code>LIKE</code> 的 substring、跟使用者 Google 預期反方向）</li>
<li>ElasticSearch 配置時 term vs match query 的選擇</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫 search feature、沒讀工具的 matching mode docs</td>
          <td>跑識別三問、確認預設</td>
      </tr>
      <tr>
          <td>使用者報「我搜 X 找不到、但是有 X」</td>
          <td>多半是 matching mode gap、不是 bug</td>
      </tr>
      <tr>
          <td>使用者打字、結果列表 0 筆、但確實有相關內容</td>
          <td>不對齊的訊號明顯、需要對策</td>
      </tr>
      <tr>
          <td>Search 跨多種使用者（Google trained / dev / DB user）</td>
          <td>Mental model 異質、選擇性高（A/B + C 組合通常需要）</td>
      </tr>
      <tr>
          <td>工具 docs 寫「matches by word prefix」這類字眼</td>
          <td>警訊 — 預設不是 substring</td>
      </tr>
      <tr>
          <td>Pagefind / Lunr / 任何 static site search</td>
          <td>預設 prefix、要主動評估是否符合需求</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：搜尋引擎的匹配模式是個容易被忽略的 capability 維度。工具預設多半是 prefix（為了 index size）、使用者預期多半是 substring 或更高（被 Google 訓練）。沒對齊 = silent 失敗：使用者誤以為內容不存在、不會 report bug。Checkpoint 1 列「使用者意圖完整集」要包含「使用者打字行為的預期」。</p>
]]></content:encoded></item><item><title>決策呈現：選項 + 推薦 + 開放修改</title><link>https://tarrragon.github.io/blog/report/decision-presentation-options-recommendation/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/decision-presentation-options-recommendation/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>把決策交給使用者時、用三層格式：&lt;strong>選項列表 + 每選項適配性 + 標出推薦 + 「想改？」開放&lt;/strong>。不要用「你覺得呢？」「你想怎麼做？」這類開放問。&lt;/p>
&lt;p>開放問看似尊重、實際把「整理問題」的成本完全丟給使用者；推薦看似越權、實際讓使用者用「同意 / 反對」這個低成本動作完成決策。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼開放問是反模式">為什麼開放問是反模式&lt;/h2>
&lt;p>開放問（&amp;ldquo;你想怎麼做？&amp;quot;）預設使用者&lt;strong>已經完成&lt;/strong>這幾步：&lt;/p>
&lt;ol>
&lt;li>知道有哪些選項&lt;/li>
&lt;li>每個選項的成本與風險&lt;/li>
&lt;li>哪個選項最適合這個情境&lt;/li>
&lt;li>願意在當下重新整理思緒、把答案寫成完整指令&lt;/li>
&lt;/ol>
&lt;p>如果使用者已經做完這四步、他不會找你來問 — 他會直接下指令。會走到「需要決策」這個動作、通常是因為&lt;strong>手上沒有完整 1-3&lt;/strong>、需要對方協助整理。&lt;/p>
&lt;p>開放問 = 把「整理選項」這件事再丟回去、讓使用者重新做一遍你已經做過的功課。&lt;/p>
&lt;hr>
&lt;h2 id="三層格式的展開">三層格式的展開&lt;/h2>
&lt;h3 id="layer-1選項列表">Layer 1：選項列表&lt;/h3>
&lt;p>把所有合理選項列出（包含「不做」這個選項、如果合理）。每個選項一行、不超過 6 個 — 超過代表還沒篩選完、不該丟給使用者。&lt;/p>
&lt;h3 id="layer-2適配性--取捨">Layer 2：適配性 / 取捨&lt;/h3>
&lt;p>每個選項配一句「為什麼適合 / 不適合」 — 維度可以是成本、風險、相容性、複雜度等、依任務選 1-2 個維度。&lt;/p>
&lt;h3 id="layer-3標推薦--開放修改">Layer 3：標推薦 + 開放修改&lt;/h3>
&lt;p>明確說「我推薦 X、因為 Y」、然後加一句「想改成其他選項或調整、跟我說」。推薦不是越權、是&lt;strong>把判斷攤開供質疑&lt;/strong> — 使用者反對時知道反對什麼、同意時知道同意了什麼。&lt;/p>
&lt;hr>
&lt;h2 id="完整模板">完整模板&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">我看到三個方向：
&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">| A 用 X 庫 | 既有依賴、易維護 | 功能受限、要小妥協 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">| B 自己寫 | 完全控制 | 維護成本、邊界 case 要寫測試 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">| C 不做 | 0 成本 | 使用者繼續手動 |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">我會選 **A**、因為這個 feature 不是核心、用既有依賴的維護負擔最低。想改成 B 或補充 C 沒問題、跟我說。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵：使用者只需要回 &amp;ldquo;好&amp;rdquo;（同意）或 &amp;ldquo;改 B、原因是 ⋯⋯&amp;quot;（反對 + 提供新訊息）— 不必重新整理整個問題空間。&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;/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 或 B、你選哪個？&amp;rdquo;&lt;/td>
 &lt;td>沒講 A/B 差異、使用者要自己 reverse engineer&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>補 reason、讓推薦可質疑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;我覺得 A 比較好、不過 B 也行&amp;rdquo;&lt;/td>
 &lt;td>推薦不夠明確、使用者不知道你真的傾向哪邊&lt;/td>
 &lt;td>標明推薦、別騎牆&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>給 10+ 選項&lt;/td>
 &lt;td>篩選沒做完、認知超載&lt;/td>
 &lt;td>自己先篩到 ≤ 5、剩下歸為「其他可討論」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;你授權我做嗎？&amp;rdquo;&lt;/td>
 &lt;td>Yes/No 二選、缺中間態&lt;/td>
 &lt;td>給選項表、讓使用者選擇執行範圍&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>探索期、選項本身還沒成形&lt;/td>
 &lt;td>還沒到「該選」的階段、需要先 brainstorm&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者明確說「我來決定方向、你別給意見」&lt;/td>
 &lt;td>推薦會干擾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主觀偏好題（命名、配色、文字風格）&lt;/td>
 &lt;td>沒「客觀適配性」可比較&lt;/td>
 &lt;/tr>
 &lt;tr>
 &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;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡&lt;/th>
 &lt;th>關係&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../filter-instruction-clarification/">#58 模糊指令的篩選三問&lt;/a>&lt;/td>
 &lt;td>#58 是「使用者下了模糊指令、你列三問澄清」、本卡是「你列了選項、用三層格式呈現」— 同一條協議的兩端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣&lt;/a>&lt;/td>
 &lt;td>#59 的「五策略 × 適配性表」就是本卡 Layer 1+2 的具體展現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>開放問是「容易寫」的格式（少打字）、跟使用者意圖對齊（不重做功課）反相關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發&lt;/a>&lt;/td>
 &lt;td>「列選項 + 標推薦」是高 ROI 但無觸發的工作（多打字、慢）、需要協議結構強制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../decision-dialogue-dimensions/">#79 決策對話的五維度&lt;/a>&lt;/td>
 &lt;td>本卡是 #79「呈現格式」維度的展開 — 開放問 vs 結構表 + 推薦&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>寫到「你想怎麼做？」&lt;/td>
 &lt;td>改成三層格式、把選項與推薦攤開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推薦的 reason 寫成「我覺得 A 比較好」&lt;/td>
 &lt;td>補實質判準（成本 / 風險 / 相容性）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次列 7+ 選項&lt;/td>
 &lt;td>自己沒篩夠、再過濾一遍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回 &amp;ldquo;都可以、你決定&amp;rdquo;&lt;/td>
 &lt;td>推薦不夠明確、改成「我做 X、除非你反對」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推薦後使用者每次都同意&lt;/td>
 &lt;td>推薦變單純的「你決定」、檢查使用者是否還有實質參與空間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回「為什麼？」追問 reason&lt;/td>
 &lt;td>reason 寫不夠清楚、補強&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：決策呈現的目標不是「讓使用者參與」、是「讓使用者用最低成本參與」。低成本來自選項已經整理好、推薦已經攤開、只需要說「同意」或「改 X」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>把決策交給使用者時、用三層格式：<strong>選項列表 + 每選項適配性 + 標出推薦 + 「想改？」開放</strong>。不要用「你覺得呢？」「你想怎麼做？」這類開放問。</p>
<p>開放問看似尊重、實際把「整理問題」的成本完全丟給使用者；推薦看似越權、實際讓使用者用「同意 / 反對」這個低成本動作完成決策。</p>
<hr>
<h2 id="為什麼開放問是反模式">為什麼開放問是反模式</h2>
<p>開放問（&ldquo;你想怎麼做？&quot;）預設使用者<strong>已經完成</strong>這幾步：</p>
<ol>
<li>知道有哪些選項</li>
<li>每個選項的成本與風險</li>
<li>哪個選項最適合這個情境</li>
<li>願意在當下重新整理思緒、把答案寫成完整指令</li>
</ol>
<p>如果使用者已經做完這四步、他不會找你來問 — 他會直接下指令。會走到「需要決策」這個動作、通常是因為<strong>手上沒有完整 1-3</strong>、需要對方協助整理。</p>
<p>開放問 = 把「整理選項」這件事再丟回去、讓使用者重新做一遍你已經做過的功課。</p>
<hr>
<h2 id="三層格式的展開">三層格式的展開</h2>
<h3 id="layer-1選項列表">Layer 1：選項列表</h3>
<p>把所有合理選項列出（包含「不做」這個選項、如果合理）。每個選項一行、不超過 6 個 — 超過代表還沒篩選完、不該丟給使用者。</p>
<h3 id="layer-2適配性--取捨">Layer 2：適配性 / 取捨</h3>
<p>每個選項配一句「為什麼適合 / 不適合」 — 維度可以是成本、風險、相容性、複雜度等、依任務選 1-2 個維度。</p>
<h3 id="layer-3標推薦--開放修改">Layer 3：標推薦 + 開放修改</h3>
<p>明確說「我推薦 X、因為 Y」、然後加一句「想改成其他選項或調整、跟我說」。推薦不是越權、是<strong>把判斷攤開供質疑</strong> — 使用者反對時知道反對什麼、同意時知道同意了什麼。</p>
<hr>
<h2 id="完整模板">完整模板</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">我看到三個方向：
</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 用 X 庫 | 既有依賴、易維護 | 功能受限、要小妥協 |
</span></span><span class="line"><span class="ln">6</span><span class="cl">| B 自己寫 | 完全控制 | 維護成本、邊界 case 要寫測試 |
</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">
</span></span><span class="line"><span class="ln">9</span><span class="cl">我會選 **A**、因為這個 feature 不是核心、用既有依賴的維護負擔最低。想改成 B 或補充 C 沒問題、跟我說。</span></span></code></pre></div><p>關鍵：使用者只需要回 &ldquo;好&rdquo;（同意）或 &ldquo;改 B、原因是 ⋯⋯&quot;（反對 + 提供新訊息）— 不必重新整理整個問題空間。</p>
<hr>
<h2 id="反模式對照">反模式對照</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 或 B、你選哪個？&rdquo;</td>
          <td>沒講 A/B 差異、使用者要自己 reverse engineer</td>
          <td>補上每選項的適配性</td>
      </tr>
      <tr>
          <td>&ldquo;我建議 A&rdquo;（沒講為什麼）</td>
          <td>推薦不可質疑、使用者只能盲信或盲拒</td>
          <td>補 reason、讓推薦可質疑</td>
      </tr>
      <tr>
          <td>&ldquo;我覺得 A 比較好、不過 B 也行&rdquo;</td>
          <td>推薦不夠明確、使用者不知道你真的傾向哪邊</td>
          <td>標明推薦、別騎牆</td>
      </tr>
      <tr>
          <td>給 10+ 選項</td>
          <td>篩選沒做完、認知超載</td>
          <td>自己先篩到 ≤ 5、剩下歸為「其他可討論」</td>
      </tr>
      <tr>
          <td>&ldquo;你授權我做嗎？&rdquo;</td>
          <td>Yes/No 二選、缺中間態</td>
          <td>給選項表、讓使用者選擇執行範圍</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時可以例外純開放問是合理的">何時可以例外（純開放問是合理的）</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼開放問合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>探索期、選項本身還沒成形</td>
          <td>還沒到「該選」的階段、需要先 brainstorm</td>
      </tr>
      <tr>
          <td>使用者明確說「我來決定方向、你別給意見」</td>
          <td>推薦會干擾</td>
      </tr>
      <tr>
          <td>主觀偏好題（命名、配色、文字風格）</td>
          <td>沒「客觀適配性」可比較</td>
      </tr>
      <tr>
          <td>使用者已給出完整偏好、執行細節純客戶化</td>
          <td>不需要推薦、純執行</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="../filter-instruction-clarification/">#58 模糊指令的篩選三問</a></td>
          <td>#58 是「使用者下了模糊指令、你列三問澄清」、本卡是「你列了選項、用三層格式呈現」— 同一條協議的兩端</td>
      </tr>
      <tr>
          <td><a href="../filter-source-composition-strategies/">#59 五策略選擇矩陣</a></td>
          <td>#59 的「五策略 × 適配性表」就是本卡 Layer 1+2 的具體展現</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>開放問是「容易寫」的格式（少打字）、跟使用者意圖對齊（不重做功課）反相關</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a></td>
          <td>「列選項 + 標推薦」是高 ROI 但無觸發的工作（多打字、慢）、需要協議結構強制</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>推薦的 reason 寫成「我覺得 A 比較好」</td>
          <td>補實質判準（成本 / 風險 / 相容性）</td>
      </tr>
      <tr>
          <td>一次列 7+ 選項</td>
          <td>自己沒篩夠、再過濾一遍</td>
      </tr>
      <tr>
          <td>使用者回 &ldquo;都可以、你決定&rdquo;</td>
          <td>推薦不夠明確、改成「我做 X、除非你反對」</td>
      </tr>
      <tr>
          <td>推薦後使用者每次都同意</td>
          <td>推薦變單純的「你決定」、檢查使用者是否還有實質參與空間</td>
      </tr>
      <tr>
          <td>使用者回「為什麼？」追問 reason</td>
          <td>reason 寫不夠清楚、補強</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：決策呈現的目標不是「讓使用者參與」、是「讓使用者用最低成本參與」。低成本來自選項已經整理好、推薦已經攤開、只需要說「同意」或「改 X」。</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>「現在不決定」是合法選項：context 不足時延後決策</title><link>https://tarrragon.github.io/blog/report/decide-later-as-valid-option/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/decide-later-as-valid-option/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>呈現決策時、預設選項清單應包含「&lt;strong>現在不決定、先做 X 再回來&lt;/strong>」這一條 — 而且要主動標出、不是等使用者自己想到。&lt;/p>
&lt;p>「立刻決定」與「拖延」之間有第三條路：&lt;strong>結構性延後&lt;/strong>。延後有明確條件（例：等卡片補完、等 context 收斂、等下個 sprint），不是「再說啦」。沒主動給這個選項、使用者會被迫在 context 不足下做決策、產生品質低的選擇。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼立刻決定是預設卻常常錯">為什麼「立刻決定」是預設、卻常常錯&lt;/h2>
&lt;p>被問到時、對話的隱含壓力是「該答了」。這個壓力來自：&lt;/p>
&lt;ul>
&lt;li>對話節奏（沒答 = 流程卡住）&lt;/li>
&lt;li>禮貌（不答 = 不尊重對方）&lt;/li>
&lt;li>LLM / agent 預設「使用者問就執行」（沒延後機制）&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="類別-1依賴未完成的-context">類別 1：依賴未完成的 context&lt;/h3>
&lt;p>需要先讀某些 code / 跑某些測試 / 看某些資料才能判斷。例：&lt;/p>
&lt;ul>
&lt;li>「該用 strategy A 還是 B」依賴 A/B 各自的 cost — 還沒量&lt;/li>
&lt;li>「卡片 X 該寫成 pattern 還是原則」依賴知識庫整體形狀 — 還沒看&lt;/li>
&lt;li>「ship D 還是先做 B/C」依賴 D 的實作風險 — 還沒展開&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>延後條件&lt;/strong>：補完 context 即可決。&lt;/p>
&lt;h3 id="類別-2依賴尚未發生的事件">類別 2：依賴尚未發生的事件&lt;/h3>
&lt;p>需要等某個外部事件（其他 PR merge、其他人決策、某個觀測週期結束）。例：&lt;/p>
&lt;ul>
&lt;li>「這個 feature 要不要保留」依賴使用者使用率 — 等 telemetry&lt;/li>
&lt;li>「該不該 refactor X」依賴 Y team 的 migration 進度&lt;/li>
&lt;li>「flag 何時拔掉」依賴觀測期長度&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>延後條件&lt;/strong>：事件發生 / 觀測期到。&lt;/p>
&lt;h3 id="類別-3依賴上層決策">類別 3：依賴上層決策&lt;/h3>
&lt;p>某個下層決策還在等上層決策、現在做下層 = 為上層猜測、可能要重做。例：&lt;/p>
&lt;ul>
&lt;li>「這個 module 該怎麼分」依賴整體架構方向 — 還在討論中&lt;/li>
&lt;li>「DB schema 怎麼設計」依賴功能範圍是否擴張&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>延後條件&lt;/strong>：上層決策落地。&lt;/p>
&lt;hr>
&lt;h2 id="主動提供不決定選項的範本">主動提供「不決定」選項的範本&lt;/h2>
&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">|---|---|
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">| A ⋯⋯ | ⋯⋯ |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">| B ⋯⋯ | ⋯⋯ |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">| C ⋯⋯ | ⋯⋯ |
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">| **延後（補 X 再決）** | 不立刻決、先 ⋯⋯、回來時 context 完整 |
&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">我推薦 A、不過如果 ⋯⋯（某個 context 還沒展開）、我建議先延後、補完 X 再回來決。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵：&lt;strong>主動標出延後條件&lt;/strong> — 「補完 X」是具體可執行的動作、不是「再說啦」。延後不是 escape hatch、是有明確 next step 的另一種決策。&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;/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;a href="../external-trigger-for-high-roi-work/">#72 結構性跳過&lt;/a>&lt;/td>
 &lt;td>延後條件具體化、寫成 trigger&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「不決定 = 不負責」道德判斷&lt;/td>
 &lt;td>阻止使用者用合理選項&lt;/td>
 &lt;td>區分「逃避決策」vs「結構性延後」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一直 retry「那你決定了嗎？」&lt;/td>
 &lt;td>對方沒能力決也催不出來&lt;/td>
 &lt;td>改問「現在缺什麼？要不要先補 X」&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;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>Incident / 緊急修復&lt;/td>
 &lt;td>延後成本 &amp;gt; 決策品質損失&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>無關緊要的小決策（檔名、次要色）&lt;/td>
 &lt;td>決策成本 &amp;gt; 改錯成本、隨便決即可&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已經循環討論過 N 次&lt;/td>
 &lt;td>延後變藉口、強制做出 best-guess&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>等了幾天 / 幾週 context 還沒補齊&lt;/td>
 &lt;td>結構問題、不是延後解決得了的&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要 user 體驗才能驗證的&lt;/td>
 &lt;td>「決定 + ship + 看反應」比延後更快&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四類共同：&lt;strong>延後的成本 &amp;gt; 決策品質的收益&lt;/strong>。其他情境保留延後選項。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>呈現決策時、預設選項清單應包含「<strong>現在不決定、先做 X 再回來</strong>」這一條 — 而且要主動標出、不是等使用者自己想到。</p>
<p>「立刻決定」與「拖延」之間有第三條路：<strong>結構性延後</strong>。延後有明確條件（例：等卡片補完、等 context 收斂、等下個 sprint），不是「再說啦」。沒主動給這個選項、使用者會被迫在 context 不足下做決策、產生品質低的選擇。</p>
<hr>
<h2 id="為什麼立刻決定是預設卻常常錯">為什麼「立刻決定」是預設、卻常常錯</h2>
<p>被問到時、對話的隱含壓力是「該答了」。這個壓力來自：</p>
<ul>
<li>對話節奏（沒答 = 流程卡住）</li>
<li>禮貌（不答 = 不尊重對方）</li>
<li>LLM / agent 預設「使用者問就執行」（沒延後機制）</li>
<li>「快速決策 = 高效」的迷思</li>
</ul>
<p>這四條都不必然成立、合在一起變成預設。實際上<strong>有的決策本來就不該現在做</strong> — 缺資訊、缺驗證、缺其他關聯決策的結果。在這種情境下「立刻決定」= 在錯誤時點做、品質差、後續還要重做。</p>
<hr>
<h2 id="三類該延後的決策">三類該延後的決策</h2>
<h3 id="類別-1依賴未完成的-context">類別 1：依賴未完成的 context</h3>
<p>需要先讀某些 code / 跑某些測試 / 看某些資料才能判斷。例：</p>
<ul>
<li>「該用 strategy A 還是 B」依賴 A/B 各自的 cost — 還沒量</li>
<li>「卡片 X 該寫成 pattern 還是原則」依賴知識庫整體形狀 — 還沒看</li>
<li>「ship D 還是先做 B/C」依賴 D 的實作風險 — 還沒展開</li>
</ul>
<p><strong>延後條件</strong>：補完 context 即可決。</p>
<h3 id="類別-2依賴尚未發生的事件">類別 2：依賴尚未發生的事件</h3>
<p>需要等某個外部事件（其他 PR merge、其他人決策、某個觀測週期結束）。例：</p>
<ul>
<li>「這個 feature 要不要保留」依賴使用者使用率 — 等 telemetry</li>
<li>「該不該 refactor X」依賴 Y team 的 migration 進度</li>
<li>「flag 何時拔掉」依賴觀測期長度</li>
</ul>
<p><strong>延後條件</strong>：事件發生 / 觀測期到。</p>
<h3 id="類別-3依賴上層決策">類別 3：依賴上層決策</h3>
<p>某個下層決策還在等上層決策、現在做下層 = 為上層猜測、可能要重做。例：</p>
<ul>
<li>「這個 module 該怎麼分」依賴整體架構方向 — 還在討論中</li>
<li>「DB schema 怎麼設計」依賴功能範圍是否擴張</li>
</ul>
<p><strong>延後條件</strong>：上層決策落地。</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">| A ⋯⋯ | ⋯⋯ |
</span></span><span class="line"><span class="ln">4</span><span class="cl">| B ⋯⋯ | ⋯⋯ |
</span></span><span class="line"><span class="ln">5</span><span class="cl">| C ⋯⋯ | ⋯⋯ |
</span></span><span class="line"><span class="ln">6</span><span class="cl">| **延後（補 X 再決）** | 不立刻決、先 ⋯⋯、回來時 context 完整 |
</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">我推薦 A、不過如果 ⋯⋯（某個 context 還沒展開）、我建議先延後、補完 X 再回來決。</span></span></code></pre></div><p>關鍵：<strong>主動標出延後條件</strong> — 「補完 X」是具體可執行的動作、不是「再說啦」。延後不是 escape hatch、是有明確 next step 的另一種決策。</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>把「我先想想」當拖延、加壓</td>
          <td>使用者被迫在不足下決策</td>
          <td>接受延後、問「需要先補什麼」</td>
      </tr>
      <tr>
          <td>延後沒寫條件、變「之後再說」</td>
          <td><a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a></td>
          <td>延後條件具體化、寫成 trigger</td>
      </tr>
      <tr>
          <td>「不決定 = 不負責」道德判斷</td>
          <td>阻止使用者用合理選項</td>
          <td>區分「逃避決策」vs「結構性延後」</td>
      </tr>
      <tr>
          <td>一直 retry「那你決定了嗎？」</td>
          <td>對方沒能力決也催不出來</td>
          <td>改問「現在缺什麼？要不要先補 X」</td>
      </tr>
      <tr>
          <td>延後選項只給自己、不給使用者</td>
          <td>雙標、使用者沒同等權利</td>
          <td>互相對等、雙向皆可延後</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時不該延後">何時不該延後</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Incident / 緊急修復</td>
          <td>延後成本 &gt; 決策品質損失</td>
      </tr>
      <tr>
          <td>無關緊要的小決策（檔名、次要色）</td>
          <td>決策成本 &gt; 改錯成本、隨便決即可</td>
      </tr>
      <tr>
          <td>已經循環討論過 N 次</td>
          <td>延後變藉口、強制做出 best-guess</td>
      </tr>
      <tr>
          <td>等了幾天 / 幾週 context 還沒補齊</td>
          <td>結構問題、不是延後解決得了的</td>
      </tr>
      <tr>
          <td>需要 user 體驗才能驗證的</td>
          <td>「決定 + ship + 看反應」比延後更快</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-instruction-clarification/">#58 模糊指令的篩選三問</a></td>
          <td>三問之一就是「現在做 vs 等更多資訊」、本卡是這個維度的展開</td>
      </tr>
      <tr>
          <td><a href="../decision-presentation-options-recommendation/">#74 決策呈現格式</a></td>
          <td>三層格式中「選項列表」應包含「延後」這個選項</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>延後若沒 trigger 會變「結構性跳過」、必須寫條件</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>Checkpoint 1（寫之前）有時候答案就是「還不能寫、先補 context」</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>失敗 2 次後常該延後決策、回頭驗證假設</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>接受、問「要不要先補 X」</td>
      </tr>
      <tr>
          <td>使用者反覆改變決定</td>
          <td>可能 context 不足、提議延後到 X 補齊</td>
      </tr>
      <tr>
          <td>自己（agent）每次都立刻答</td>
          <td>檢查是否真的有資訊判斷、不是的話主動標延後</td>
      </tr>
      <tr>
          <td>決策表沒「不決定」欄</td>
          <td>補上、且寫具體條件</td>
      </tr>
      <tr>
          <td>「下次再決」沒寫 trigger</td>
          <td>寫條件 — 補完 X / 等到 Y / 跑完 Z 觀測</td>
      </tr>
      <tr>
          <td>一個決策卡了很久、團隊各自堅持</td>
          <td>不是延後的問題、是缺 deciding mechanism</td>
      </tr>
      <tr>
          <td>「我覺得 A 比較好不過你決定」騎牆</td>
          <td>不夠明確的推薦 + 延後混在一起、區分清楚</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：對話中「答 / 不答」是二元的、決策中「決 / 延後 / 拒絕決」是三元的。把延後當合法選項主動提供、品質會比強迫立刻決更好。延後不是禮貌性給出口、是工程上對「context 不足」的正確反應。</p>
]]></content:encoded></item><item><title>反省任務預設複選：互斥要證明、不互斥是預設</title><link>https://tarrragon.github.io/blog/report/retrospective-multi-select-default/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/retrospective-multi-select-default/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫到「我們還可以做什麼？」「下一步該往哪走？」「這次反省我們學到 X、Y、Z 哪個重要？」這類&lt;strong>反省 / 改進方向&lt;/strong>問題、預設給&lt;strong>複選&lt;/strong>（checkbox semantics、可選多項）。&lt;/p>
&lt;p>執行類決策多半互斥（用 A 工具 vs B 工具）、反省類決策多半不互斥（思考 X + 思考 Y + 思考 Z 都做、互不干擾、合在一起反省深度更高）。把反省題用單選格式呈現 = 強迫使用者排序「思考的優先級」、結果通常 1 被選、2-N 被丟。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼反省題不該單選">為什麼反省題不該單選&lt;/h2>
&lt;p>反省任務的 output 通常是「對問題的多面向理解 / 多條後續動作」、不是「一個結論」。多選項共存時：&lt;/p>
&lt;ul>
&lt;li>思考 A 跟思考 B 互不干擾（同一個事件可從多角度反省）&lt;/li>
&lt;li>動作 X 跟動作 Y 互補（補卡片 + 寫測試 + 改流程）&lt;/li>
&lt;li>多角度疊加 → 反省深度更高、不是「一條最佳」就夠&lt;/li>
&lt;/ul>
&lt;p>把這種題目強制成單選、暗示「只能挑一個」、使用者就只挑一個。其他 N-1 個本來該做的、被結構性跳過 — 不是因為不重要、是因為&lt;strong>呈現格式叫他這樣選&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="反省-vs-執行呈現格式的差異">反省 vs 執行：呈現格式的差異&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>互斥性&lt;/td>
 &lt;td>通常互斥（slot 只能放一個）&lt;/td>
 &lt;td>通常不互斥（多角度共存）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>預設格式&lt;/td>
 &lt;td>單選 + 推薦（&lt;a href="../decision-presentation-options-recommendation/">#74&lt;/a>）&lt;/td>
 &lt;td>複選 + 全列&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推薦語氣&lt;/td>
 &lt;td>「我建議 A」&lt;/td>
 &lt;td>「都該做、優先 X」或「全做」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回應&lt;/td>
 &lt;td>同意 / 改 B&lt;/td>
 &lt;td>「全做」/「先 1+2、3 下輪」/「跳過 X」&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;strong>識別這個決策是「執行」還是「反省」&lt;/strong>、決定用哪種格式。&lt;/p>
&lt;hr>
&lt;h2 id="完整模板反省題">完整模板（反省題）&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">這次經歷我們可以反省幾個方向：
&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">1. 補卡片做分析（沉澱知識）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">2. 補測試固化（避免回退）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">3. 改流程 / 工具觸發（結構性對策）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">4. 整理 case study 文章（對外輸出）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">5. 重做某個決策（如果發現方向錯）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">這些**互不衝突、可以全做**。如果時間有限、我建議優先 1+2（沉澱 + 固化），3+4 下輪。要全做、跳過某幾個、或調整順序、跟我說。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵差異 vs 執行類決策：&lt;/p>
&lt;ul>
&lt;li>明示「互不衝突、可以全做」&lt;/li>
&lt;li>推薦是「優先級」而非「選一個」&lt;/li>
&lt;li>「全做」是合法選項 + 「跳過某幾個」也是合法選項&lt;/li>
&lt;/ul>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>反省題用 radio 格式（&amp;ldquo;你想做哪一個？&amp;quot;）&lt;/td>
 &lt;td>強迫單選、丟失多面向&lt;/td>
 &lt;td>改 checkbox 格式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不講「可以全選」、使用者預設選一個&lt;/td>
 &lt;td>隱式單選&lt;/td>
 &lt;td>主動標「互不衝突、全做也可以」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>列 5 個方向、第一個是顯然要做的&lt;/td>
 &lt;td>排序暗示 = 隱式推薦、其他被忽略&lt;/td>
 &lt;td>區分「全選 vs 優先選」、明示順序&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「主要是 1、2-5 也可以做」騎牆&lt;/td>
 &lt;td>推薦不夠明確&lt;/td>
 &lt;td>標清楚 + 講優先級&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次列 10+ 個方向&lt;/td>
 &lt;td>認知超載、變成「都不選」&lt;/td>
 &lt;td>自己先聚類、≤ 7 項&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反省結論寫成「我們學到 X」（單一）&lt;/td>
 &lt;td>多面向 collapse 成單點&lt;/td>
 &lt;td>列出所有面向、再標核心&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>多選項真的互斥（不同優先級會排擠）&lt;/td>
 &lt;td>例：「先做 X 還是先做 Y、人力只夠一個」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反省結論本身互斥（A 對 = B 錯）&lt;/td>
 &lt;td>例：「root cause 是 P 還是 Q」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者明確要單選&lt;/td>
 &lt;td>尊重使用者的決策框架&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>二選一的對比題&lt;/td>
 &lt;td>例：「這次失誤是設計問題還是執行問題」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四類共通：&lt;strong>互斥性是真實的、不是格式造成的&lt;/strong>。其他情境預設複選。&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>&lt;a href="../decision-presentation-options-recommendation/">#74 決策呈現格式&lt;/a>&lt;/td>
 &lt;td>#74 是執行類決策的格式、本卡是反省類決策的特化 — 兩卡互補&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項&lt;/a>&lt;/td>
 &lt;td>反省題的「跳過某幾個」常是隱式延後、本卡跟 #77 對應&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../verification-timeline-checkpoints/">#68 驗收的時間軸&lt;/a>&lt;/td>
 &lt;td>Ship 後反省 = 多面向、適用本卡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>「列複選 + 標優先級」比「列一個推薦」難寫、容易被簡化成單選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../decision-dialogue-dimensions/">#79 決策對話的五維度&lt;/a>&lt;/td>
 &lt;td>本卡是 #79「選項類型」維度的展開 — 單選 radio vs 複選 checkbox&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>反省題只列「最該做的一個」&lt;/td>
 &lt;td>補上其他面向、改複選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者回應 &amp;ldquo;1+2&amp;rdquo;、&amp;ldquo;全做&amp;rdquo;、&amp;ldquo;1234&amp;rdquo;&lt;/td>
 &lt;td>確認你給了複選格式、否則使用者得自己 reverse&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者只回 &amp;ldquo;1&amp;rdquo;、然後其他面向沒做到&lt;/td>
 &lt;td>檢查格式是否暗示了單選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反省結論 collapse 成一句話&lt;/td>
 &lt;td>退一步、列出多面向再寫總結&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>某面向「明顯該做」、自己已替使用者選&lt;/td>
 &lt;td>取消代選、明示「都該做、想跳過告訴我」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「先 X、Y 之後再做」每次 Y 都不做&lt;/td>
 &lt;td>跟 &lt;a href="../external-trigger-for-high-roi-work/">#72&lt;/a> 一樣、補 trigger&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>列 10+ 個面向&lt;/td>
 &lt;td>自己聚類、別丟給使用者&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：執行決定是「選一個 path」、反省決定是「攤開多面向 + 標優先級」。預設互斥的格式（radio）套用到不互斥的場景（反省）= 結構性把多面向 collapse 成單點、丟失反省應有的深度。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫到「我們還可以做什麼？」「下一步該往哪走？」「這次反省我們學到 X、Y、Z 哪個重要？」這類<strong>反省 / 改進方向</strong>問題、預設給<strong>複選</strong>（checkbox semantics、可選多項）。</p>
<p>執行類決策多半互斥（用 A 工具 vs B 工具）、反省類決策多半不互斥（思考 X + 思考 Y + 思考 Z 都做、互不干擾、合在一起反省深度更高）。把反省題用單選格式呈現 = 強迫使用者排序「思考的優先級」、結果通常 1 被選、2-N 被丟。</p>
<hr>
<h2 id="為什麼反省題不該單選">為什麼反省題不該單選</h2>
<p>反省任務的 output 通常是「對問題的多面向理解 / 多條後續動作」、不是「一個結論」。多選項共存時：</p>
<ul>
<li>思考 A 跟思考 B 互不干擾（同一個事件可從多角度反省）</li>
<li>動作 X 跟動作 Y 互補（補卡片 + 寫測試 + 改流程）</li>
<li>多角度疊加 → 反省深度更高、不是「一條最佳」就夠</li>
</ul>
<p>把這種題目強制成單選、暗示「只能挑一個」、使用者就只挑一個。其他 N-1 個本來該做的、被結構性跳過 — 不是因為不重要、是因為<strong>呈現格式叫他這樣選</strong>。</p>
<hr>
<h2 id="反省-vs-執行呈現格式的差異">反省 vs 執行：呈現格式的差異</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>執行類決策</th>
          <th>反省類決策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>互斥性</td>
          <td>通常互斥（slot 只能放一個）</td>
          <td>通常不互斥（多角度共存）</td>
      </tr>
      <tr>
          <td>預設格式</td>
          <td>單選 + 推薦（<a href="../decision-presentation-options-recommendation/">#74</a>）</td>
          <td>複選 + 全列</td>
      </tr>
      <tr>
          <td>推薦語氣</td>
          <td>「我建議 A」</td>
          <td>「都該做、優先 X」或「全做」</td>
      </tr>
      <tr>
          <td>使用者回應</td>
          <td>同意 / 改 B</td>
          <td>「全做」/「先 1+2、3 下輪」/「跳過 X」</td>
      </tr>
      <tr>
          <td>「全做」是合法回應</td>
          <td>通常不（資源有限 / 互斥）</td>
          <td>通常是</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>識別這個決策是「執行」還是「反省」</strong>、決定用哪種格式。</p>
<hr>
<h2 id="完整模板反省題">完整模板（反省題）</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">這次經歷我們可以反省幾個方向：
</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">1. 補卡片做分析（沉澱知識）
</span></span><span class="line"><span class="ln">4</span><span class="cl">2. 補測試固化（避免回退）
</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. 整理 case study 文章（對外輸出）
</span></span><span class="line"><span class="ln">7</span><span class="cl">5. 重做某個決策（如果發現方向錯）
</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">這些**互不衝突、可以全做**。如果時間有限、我建議優先 1+2（沉澱 + 固化），3+4 下輪。要全做、跳過某幾個、或調整順序、跟我說。</span></span></code></pre></div><p>關鍵差異 vs 執行類決策：</p>
<ul>
<li>明示「互不衝突、可以全做」</li>
<li>推薦是「優先級」而非「選一個」</li>
<li>「全做」是合法選項 + 「跳過某幾個」也是合法選項</li>
</ul>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>反省題用 radio 格式（&ldquo;你想做哪一個？&quot;）</td>
          <td>強迫單選、丟失多面向</td>
          <td>改 checkbox 格式</td>
      </tr>
      <tr>
          <td>不講「可以全選」、使用者預設選一個</td>
          <td>隱式單選</td>
          <td>主動標「互不衝突、全做也可以」</td>
      </tr>
      <tr>
          <td>列 5 個方向、第一個是顯然要做的</td>
          <td>排序暗示 = 隱式推薦、其他被忽略</td>
          <td>區分「全選 vs 優先選」、明示順序</td>
      </tr>
      <tr>
          <td>「主要是 1、2-5 也可以做」騎牆</td>
          <td>推薦不夠明確</td>
          <td>標清楚 + 講優先級</td>
      </tr>
      <tr>
          <td>一次列 10+ 個方向</td>
          <td>認知超載、變成「都不選」</td>
          <td>自己先聚類、≤ 7 項</td>
      </tr>
      <tr>
          <td>反省結論寫成「我們學到 X」（單一）</td>
          <td>多面向 collapse 成單點</td>
          <td>列出所有面向、再標核心</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時反省題該回退到單選">何時反省題該回退到單選</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多選項真的互斥（不同優先級會排擠）</td>
          <td>例：「先做 X 還是先做 Y、人力只夠一個」</td>
      </tr>
      <tr>
          <td>反省結論本身互斥（A 對 = B 錯）</td>
          <td>例：「root cause 是 P 還是 Q」</td>
      </tr>
      <tr>
          <td>使用者明確要單選</td>
          <td>尊重使用者的決策框架</td>
      </tr>
      <tr>
          <td>二選一的對比題</td>
          <td>例：「這次失誤是設計問題還是執行問題」</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="../decision-presentation-options-recommendation/">#74 決策呈現格式</a></td>
          <td>#74 是執行類決策的格式、本卡是反省類決策的特化 — 兩卡互補</td>
      </tr>
      <tr>
          <td><a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項</a></td>
          <td>反省題的「跳過某幾個」常是隱式延後、本卡跟 #77 對應</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>Ship 後反省 = 多面向、適用本卡</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>「列複選 + 標優先級」比「列一個推薦」難寫、容易被簡化成單選</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79「選項類型」維度的展開 — 單選 radio vs 複選 checkbox</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>使用者回應 &ldquo;1+2&rdquo;、&ldquo;全做&rdquo;、&ldquo;1234&rdquo;</td>
          <td>確認你給了複選格式、否則使用者得自己 reverse</td>
      </tr>
      <tr>
          <td>使用者只回 &ldquo;1&rdquo;、然後其他面向沒做到</td>
          <td>檢查格式是否暗示了單選</td>
      </tr>
      <tr>
          <td>反省結論 collapse 成一句話</td>
          <td>退一步、列出多面向再寫總結</td>
      </tr>
      <tr>
          <td>某面向「明顯該做」、自己已替使用者選</td>
          <td>取消代選、明示「都該做、想跳過告訴我」</td>
      </tr>
      <tr>
          <td>「先 X、Y 之後再做」每次 Y 都不做</td>
          <td>跟 <a href="../external-trigger-for-high-roi-work/">#72</a> 一樣、補 trigger</td>
      </tr>
      <tr>
          <td>列 10+ 個面向</td>
          <td>自己聚類、別丟給使用者</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：執行決定是「選一個 path」、反省決定是「攤開多面向 + 標優先級」。預設互斥的格式（radio）套用到不互斥的場景（反省）= 結構性把多面向 collapse 成單點、丟失反省應有的深度。</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>Yes/No 二選是隱式 collapse：把多選空間壓成 1 bit</title><link>https://tarrragon.github.io/blog/report/yes-no-binary-collapse/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/yes-no-binary-collapse/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>把決策呈現成 yes/no 問句（&amp;ldquo;需要我繼續嗎？&amp;quot;、&amp;ldquo;要做嗎？&amp;quot;、&amp;ldquo;確認嗎？&amp;quot;）= 把使用者的回應空間壓縮到 1 bit。&lt;strong>1 bit 不夠表達決策&lt;/strong>：使用者實際上有「改方向 / 延後 / 疊加 / 分批 / 反問」等合法選項、被 yes/no 結構藏起來。&lt;/p>
&lt;p>這是 &lt;a href="../decision-dialogue-dimensions/">#79 五維度&lt;/a> collapse 的極致形態 — 預設窄格 + 預設立刻 + 預設單選 + 預設一次完成 + 預設單策略 + &lt;strong>再加一層 reduce 到 binary&lt;/strong>。最容易寫、最隱形、最常見。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-yesno-看起來合理但其實不夠">為什麼 Yes/No 看起來合理但其實不夠&lt;/h2>
&lt;p>Yes/No 問句的隱含預設：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>使用者已經同意「我提出的方向」是對的&lt;/strong> — 只剩執行 / 不執行&lt;/li>
&lt;li>&lt;strong>沒有「微調」的中間態&lt;/strong> — 想改一點點、要重起一輪 yes/no&lt;/li>
&lt;li>&lt;strong>沒有「再多想想」&lt;/strong> — 沒有 yes 也沒有 no 的時候被迫硬選&lt;/li>
&lt;li>&lt;strong>沒有「順便加 X」&lt;/strong> — 想疊加策略要破格自己提&lt;/li>
&lt;li>&lt;strong>沒有「先做一半」&lt;/strong> — 全做 vs 不做、沒有分批選項&lt;/li>
&lt;/ol>
&lt;p>實際上多數決策不是 1 bit 寬。當 agent 寫「要繼續嗎？」、使用者經常回答「等等、我想先 X」、「順便也做 Y」、「先做 A 不做 B」 — 這些回應全部都不是 yes 也不是 no、是&lt;strong>對話越過 yes/no 結構自我修正&lt;/strong>。&lt;/p>
&lt;p>修正成本由使用者承擔 = 反過來 #74 的問題（把整理成本丟回使用者）。&lt;/p>
&lt;hr>
&lt;h2 id="yesno-的常見變種">Yes/No 的常見變種&lt;/h2>
&lt;p>agent 容易寫的 yes/no 變種：&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>&amp;ldquo;需要我 X 嗎？&amp;rdquo;&lt;/td>
 &lt;td>我已經想好要 X、你只要批准&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;要繼續嗎？&amp;rdquo;&lt;/td>
 &lt;td>流程往下走、你只要不阻止&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;OK 嗎？&amp;rdquo;&lt;/td>
 &lt;td>我做了 X、你只要不反對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;我先做 X、有問題嗎？&amp;rdquo;&lt;/td>
 &lt;td>我打算開幹、你只要沒異議&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;確認一下、是這樣嗎？&amp;rdquo;&lt;/td>
 &lt;td>你之前說的我理解對嗎、回 yes/no&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;ldquo;如果 OK 我繼續做&amp;rdquo;&lt;/td>
 &lt;td>同意 → 繼續、不同意 → 卡住&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個變種都把使用者推進「同意 / 不同意」的二元框、其他合法回應（改 / 延後 / 疊加 / 分批 / 反問澄清）需要使用者自己破格。&lt;/p>
&lt;hr>
&lt;h2 id="修法把-yesno-翻成多維展開">修法：把 Yes/No 翻成多維展開&lt;/h2>
&lt;p>把 binary 拆回多維：&lt;/p>
&lt;h3 id="beforeyesno">Before（yes/no）&lt;/h3>
&lt;blockquote>
&lt;p>&amp;ldquo;我打算 ship D（UX hint）、需要我繼續嗎？&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;h3 id="after五維展開">After（五維展開）&lt;/h3>
&lt;blockquote>
&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>立刻 ship D&lt;/td>
 &lt;td>解眼前痛&lt;/td>
 &lt;td>不解根因&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延後 D、先補 telemetry&lt;/td>
 &lt;td>等資料&lt;/td>
 &lt;td>推遲 1 週&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跳過 D、做 B/C&lt;/td>
 &lt;td>結構 fix&lt;/td>
 &lt;td>風險高、要驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>D + 寫測試固化&lt;/td>
 &lt;td>疊加&lt;/td>
 &lt;td>多寫 30 分鐘&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>我推薦 &lt;strong>D + 寫測試固化&lt;/strong>（疊加）— 解眼前痛 + 防回退。改 / 延後 / 跳過都行、跟我說。&lt;/p>&lt;/blockquote>
&lt;h3 id="對照">對照&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Before&lt;/th>
 &lt;th>After&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>呈現格式&lt;/td>
 &lt;td>yes/no 一句&lt;/td>
 &lt;td>結構表 + 推薦&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>策略數&lt;/td>
 &lt;td>單一 D&lt;/td>
 &lt;td>D + 測試疊加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>批次邊界&lt;/td>
 &lt;td>一次&lt;/td>
 &lt;td>隱含分批（B/C 下輪）&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>binary&lt;/td>
 &lt;td>多選、可疊加&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>明顯多花字、明顯讓使用者更容易做正確決策。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>把決策呈現成 yes/no 問句（&ldquo;需要我繼續嗎？&quot;、&ldquo;要做嗎？&quot;、&ldquo;確認嗎？&quot;）= 把使用者的回應空間壓縮到 1 bit。<strong>1 bit 不夠表達決策</strong>：使用者實際上有「改方向 / 延後 / 疊加 / 分批 / 反問」等合法選項、被 yes/no 結構藏起來。</p>
<p>這是 <a href="../decision-dialogue-dimensions/">#79 五維度</a> collapse 的極致形態 — 預設窄格 + 預設立刻 + 預設單選 + 預設一次完成 + 預設單策略 + <strong>再加一層 reduce 到 binary</strong>。最容易寫、最隱形、最常見。</p>
<hr>
<h2 id="為什麼-yesno-看起來合理但其實不夠">為什麼 Yes/No 看起來合理但其實不夠</h2>
<p>Yes/No 問句的隱含預設：</p>
<ol>
<li><strong>使用者已經同意「我提出的方向」是對的</strong> — 只剩執行 / 不執行</li>
<li><strong>沒有「微調」的中間態</strong> — 想改一點點、要重起一輪 yes/no</li>
<li><strong>沒有「再多想想」</strong> — 沒有 yes 也沒有 no 的時候被迫硬選</li>
<li><strong>沒有「順便加 X」</strong> — 想疊加策略要破格自己提</li>
<li><strong>沒有「先做一半」</strong> — 全做 vs 不做、沒有分批選項</li>
</ol>
<p>實際上多數決策不是 1 bit 寬。當 agent 寫「要繼續嗎？」、使用者經常回答「等等、我想先 X」、「順便也做 Y」、「先做 A 不做 B」 — 這些回應全部都不是 yes 也不是 no、是<strong>對話越過 yes/no 結構自我修正</strong>。</p>
<p>修正成本由使用者承擔 = 反過來 #74 的問題（把整理成本丟回使用者）。</p>
<hr>
<h2 id="yesno-的常見變種">Yes/No 的常見變種</h2>
<p>agent 容易寫的 yes/no 變種：</p>
<table>
  <thead>
      <tr>
          <th>變種</th>
          <th>隱含預設</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&ldquo;需要我 X 嗎？&rdquo;</td>
          <td>我已經想好要 X、你只要批准</td>
      </tr>
      <tr>
          <td>&ldquo;要繼續嗎？&rdquo;</td>
          <td>流程往下走、你只要不阻止</td>
      </tr>
      <tr>
          <td>&ldquo;OK 嗎？&rdquo;</td>
          <td>我做了 X、你只要不反對</td>
      </tr>
      <tr>
          <td>&ldquo;我先做 X、有問題嗎？&rdquo;</td>
          <td>我打算開幹、你只要沒異議</td>
      </tr>
      <tr>
          <td>&ldquo;確認一下、是這樣嗎？&rdquo;</td>
          <td>你之前說的我理解對嗎、回 yes/no</td>
      </tr>
      <tr>
          <td>&ldquo;如果 OK 我繼續做&rdquo;</td>
          <td>同意 → 繼續、不同意 → 卡住</td>
      </tr>
  </tbody>
</table>
<p>每個變種都把使用者推進「同意 / 不同意」的二元框、其他合法回應（改 / 延後 / 疊加 / 分批 / 反問澄清）需要使用者自己破格。</p>
<hr>
<h2 id="修法把-yesno-翻成多維展開">修法：把 Yes/No 翻成多維展開</h2>
<p>把 binary 拆回多維：</p>
<h3 id="beforeyesno">Before（yes/no）</h3>
<blockquote>
<p>&ldquo;我打算 ship D（UX hint）、需要我繼續嗎？&rdquo;</p></blockquote>
<h3 id="after五維展開">After（五維展開）</h3>
<blockquote>
<p>接下來的選項：</p>
<table>
  <thead>
      <tr>
          <th>選項</th>
          <th>適配性</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>立刻 ship D</td>
          <td>解眼前痛</td>
          <td>不解根因</td>
      </tr>
      <tr>
          <td>延後 D、先補 telemetry</td>
          <td>等資料</td>
          <td>推遲 1 週</td>
      </tr>
      <tr>
          <td>跳過 D、做 B/C</td>
          <td>結構 fix</td>
          <td>風險高、要驗證</td>
      </tr>
      <tr>
          <td>D + 寫測試固化</td>
          <td>疊加</td>
          <td>多寫 30 分鐘</td>
      </tr>
  </tbody>
</table>
<p>我推薦 <strong>D + 寫測試固化</strong>（疊加）— 解眼前痛 + 防回退。改 / 延後 / 跳過都行、跟我說。</p></blockquote>
<h3 id="對照">對照</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Before</th>
          <th>After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>呈現格式</td>
          <td>yes/no 一句</td>
          <td>結構表 + 推薦</td>
      </tr>
      <tr>
          <td>策略數</td>
          <td>單一 D</td>
          <td>D + 測試疊加</td>
      </tr>
      <tr>
          <td>批次邊界</td>
          <td>一次</td>
          <td>隱含分批（B/C 下輪）</td>
      </tr>
      <tr>
          <td>時間軸</td>
          <td>立刻</td>
          <td>提供延後條件</td>
      </tr>
      <tr>
          <td>選項類型</td>
          <td>binary</td>
          <td>多選、可疊加</td>
      </tr>
  </tbody>
</table>
<p>明顯多花字、明顯讓使用者更容易做正確決策。</p>
<hr>
<h2 id="何時-yesno-真的合理">何時 Yes/No 真的合理</h2>
<p>不是所有 yes/no 都該展開。合理的少數情境：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 yes/no 夠用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已經三回合確認過、純最終 confirmation</td>
          <td>多選空間在前面回合已展開</td>
      </tr>
      <tr>
          <td>Atomic 動作、沒中間態（push 與否、刪除與否）</td>
          <td>真的是 binary、沒有 1.5</td>
      </tr>
      <tr>
          <td>使用者明確下了「OK 直接做」的指令</td>
          <td>多展開反而干擾節奏</td>
      </tr>
      <tr>
          <td>緊急情境、來不及展開</td>
          <td>時間壓力 &gt; 決策品質</td>
      </tr>
      <tr>
          <td>純 sanity check、執行已經決定（&ldquo;我跑 X、有沒有要先停？&quot;）</td>
          <td>預設執行、yes/no 是 last chance 攔截</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="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79 在「最常見變種」的具體化 — yes/no 是五維全 collapse 再加 binary</td>
      </tr>
      <tr>
          <td><a href="../decision-presentation-options-recommendation/">#74 決策呈現格式</a></td>
          <td>yes/no 等同「給推薦但不給選項」、是 #74 反模式列的延伸</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>yes/no 是「最容易寫」的格式（最少字）、跟使用者意圖對齊（多選空間）反相關</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>「展開 yes/no」是高 ROI 但無觸發的工作、需要 L3 結構性對策提示</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到「需要 X 嗎？」</td>
          <td>翻成多選表</td>
      </tr>
      <tr>
          <td>寫到「要繼續嗎？」</td>
          <td>列出「改方向 / 延後 / 疊加」三選</td>
      </tr>
      <tr>
          <td>「OK 嗎？」結尾</td>
          <td>檢查是否真的是 last-mile confirm、否則展開</td>
      </tr>
      <tr>
          <td>使用者回 &ldquo;等等、先 X&rdquo;</td>
          <td>你寫了 yes/no、他在破格 — 下次該預先展開</td>
      </tr>
      <tr>
          <td>使用者回 &ldquo;順便也做 Y&rdquo;</td>
          <td>你漏了疊加維度</td>
      </tr>
      <tr>
          <td>「如果 OK 我繼續」隱含「不同意 = 卡住」</td>
          <td>使用者不同意有實際下一步嗎？沒有 = 變相強制同意</td>
      </tr>
      <tr>
          <td>一輪對話 yes/no 出現 ≥ 3 次</td>
          <td>你在用 binary 取代展開、退一步看是不是 #79 五維都該展開</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Yes/No 是 1 bit、決策空間是 N bit。<strong>用 yes/no 包裝不是「簡化」、是「藏掉維度」</strong>。簡化是少寫某個維度；藏掉維度是把多維壓成 1 bit、讓使用者破格才能表達。前者良性、後者不良。</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>Writing 的 multi-pass review：N 輪 review、每輪換 frame</title><link>https://tarrragon.github.io/blog/report/writing-multi-pass-review/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/writing-multi-pass-review/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫文字（文章 / 註解 / doc / prompt / commit message）的 ROI 來自 &lt;strong>N 輪不同 frame 的 re-read&lt;/strong>、不是單次「寫對」。每輪 catch 上一輪 frame miss 的東西：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>輪次&lt;/th>
 &lt;th>Frame&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>把 idea 變字、預期會有錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>對意圖（&lt;a href="../ease-of-writing-vs-intent-alignment/">#67&lt;/a>）&lt;/td>
 &lt;td>寫出來跟原意對齊嗎、有沒有便利驅動的偏移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>機會成本語氣&lt;/td>
 &lt;td>有沒有「應該」「不行」「正確」絕對主義？是不是該翻成「在 X 情境下 A 較好、Y 情境 B 較好」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>Grep-ability / 命名&lt;/td>
 &lt;td>關鍵字前置嗎？AI 能單次 grep 命中嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>反例 / 邊界&lt;/td>
 &lt;td>「何時不適用」段寫了嗎？反模式列了嗎？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每輪用「跟前一輪不同的眼睛」看同一份文字 — 才能 catch 不同層的問題。&lt;strong>第 1 輪的 frame 不可能同時 catch 所有層&lt;/strong>（&lt;a href="../literal-interception-vs-behavioral-refinement/">#82&lt;/a> 字面 vs 行為的 ceiling）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼一輪寫不出全部維度">為什麼一輪寫不出全部維度&lt;/h2>
&lt;p>寫的時候 working memory 有限、必須 collapse 多個維度去專注其中之一：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>生成 frame&lt;/strong> 在意 idea 完整 → 顧不上語氣&lt;/li>
&lt;li>&lt;strong>語氣 frame&lt;/strong> 在意「機會成本 vs 絕對主義」→ 顧不上 grep-ability&lt;/li>
&lt;li>&lt;strong>grep frame&lt;/strong> 在意關鍵字前置 → 顧不上反例&lt;/li>
&lt;/ul>
&lt;p>要求一輪同時 catch 所有 = 認知超載。實際結果是「每個維度都做一半」。&lt;/p>
&lt;p>&lt;strong>多輪設計接受 working memory 限制、用 N 輪解 N 維&lt;/strong>。每輪只專注一個 frame、效率反而高。&lt;/p>
&lt;hr>
&lt;h2 id="五輪-review-的具體-checklist">五輪 review 的具體 checklist&lt;/h2>
&lt;h3 id="輪-1生成">輪 1：生成&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> idea 從頭寫到尾、不停下來改&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 預期會有 typo、結構亂、語氣不對 — 不在這輪修&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跑得到結尾比寫得漂亮重要&lt;/li>
&lt;/ul>
&lt;h3 id="輪-2對意圖67">輪 2：對意圖（&lt;a href="../ease-of-writing-vs-intent-alignment/">#67&lt;/a>）&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 開頭一句講清「這段在說什麼」嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 有沒有 #67 的「便利驅動偏移」— 寫得順但其實偏題了？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 段落順序是不是「好寫」決定的、不是「易讀」決定的？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 有沒有「為了補滿格式」寫的廢段？&lt;/li>
&lt;/ul>
&lt;h3 id="輪-3機會成本語氣">輪 3：機會成本語氣&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 跑 grep 抓「應該 / 必須 / 不行 / 不可以 / 正確的方式 / 唯一」這些絕對詞&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 每個絕對詞檢查：是物理 / 法律 / 安全事實嗎？不是的話翻成「在 X 情境下 A 較好、Y 情境下 B 較好」&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 反模式表的「為什麼不好」寫到「違反某個原則」、不寫「就是不對」&lt;/li>
&lt;/ul>
&lt;h3 id="輪-4grep-ability--命名">輪 4：Grep-ability / 命名&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 關鍵字在段首、不在段中&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 表格欄位用 grep 友善的分隔（&lt;code>:&lt;/code> &lt;code>|&lt;/code> &lt;code>→&lt;/code>）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 檔名 / slug 跟 title 對應、不要用流水號&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 命名能用單次 grep 命中、不需要語意推理&lt;/li>
&lt;/ul>
&lt;h3 id="輪-5反例--邊界">輪 5：反例 / 邊界&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 「何時不適用」段寫了嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 反模式表給「為什麼不好 + 修法」嗎、還是只給警告？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跨卡 cross-link 補了嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 有沒有 over-claim、把「在多數情境下」說成「總是」？&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="套用到不同-output-類型">套用到不同 output 類型&lt;/h2>
&lt;p>每類有特化的輪次組合：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫文字（文章 / 註解 / doc / prompt / commit message）的 ROI 來自 <strong>N 輪不同 frame 的 re-read</strong>、不是單次「寫對」。每輪 catch 上一輪 frame miss 的東西：</p>
<table>
  <thead>
      <tr>
          <th>輪次</th>
          <th>Frame</th>
          <th>抓什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>生成</td>
          <td>把 idea 變字、預期會有錯</td>
      </tr>
      <tr>
          <td>2</td>
          <td>對意圖（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>）</td>
          <td>寫出來跟原意對齊嗎、有沒有便利驅動的偏移</td>
      </tr>
      <tr>
          <td>3</td>
          <td>機會成本語氣</td>
          <td>有沒有「應該」「不行」「正確」絕對主義？是不是該翻成「在 X 情境下 A 較好、Y 情境 B 較好」</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Grep-ability / 命名</td>
          <td>關鍵字前置嗎？AI 能單次 grep 命中嗎？</td>
      </tr>
      <tr>
          <td>5</td>
          <td>反例 / 邊界</td>
          <td>「何時不適用」段寫了嗎？反模式列了嗎？</td>
      </tr>
  </tbody>
</table>
<p>每輪用「跟前一輪不同的眼睛」看同一份文字 — 才能 catch 不同層的問題。<strong>第 1 輪的 frame 不可能同時 catch 所有層</strong>（<a href="../literal-interception-vs-behavioral-refinement/">#82</a> 字面 vs 行為的 ceiling）。</p>
<hr>
<h2 id="為什麼一輪寫不出全部維度">為什麼一輪寫不出全部維度</h2>
<p>寫的時候 working memory 有限、必須 collapse 多個維度去專注其中之一：</p>
<ul>
<li><strong>生成 frame</strong> 在意 idea 完整 → 顧不上語氣</li>
<li><strong>語氣 frame</strong> 在意「機會成本 vs 絕對主義」→ 顧不上 grep-ability</li>
<li><strong>grep frame</strong> 在意關鍵字前置 → 顧不上反例</li>
</ul>
<p>要求一輪同時 catch 所有 = 認知超載。實際結果是「每個維度都做一半」。</p>
<p><strong>多輪設計接受 working memory 限制、用 N 輪解 N 維</strong>。每輪只專注一個 frame、效率反而高。</p>
<hr>
<h2 id="五輪-review-的具體-checklist">五輪 review 的具體 checklist</h2>
<h3 id="輪-1生成">輪 1：生成</h3>
<ul>
<li><input disabled="" type="checkbox"> idea 從頭寫到尾、不停下來改</li>
<li><input disabled="" type="checkbox"> 預期會有 typo、結構亂、語氣不對 — 不在這輪修</li>
<li><input disabled="" type="checkbox"> 跑得到結尾比寫得漂亮重要</li>
</ul>
<h3 id="輪-2對意圖67">輪 2：對意圖（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>）</h3>
<ul>
<li><input disabled="" type="checkbox"> 開頭一句講清「這段在說什麼」嗎？</li>
<li><input disabled="" type="checkbox"> 有沒有 #67 的「便利驅動偏移」— 寫得順但其實偏題了？</li>
<li><input disabled="" type="checkbox"> 段落順序是不是「好寫」決定的、不是「易讀」決定的？</li>
<li><input disabled="" type="checkbox"> 有沒有「為了補滿格式」寫的廢段？</li>
</ul>
<h3 id="輪-3機會成本語氣">輪 3：機會成本語氣</h3>
<ul>
<li><input disabled="" type="checkbox"> 跑 grep 抓「應該 / 必須 / 不行 / 不可以 / 正確的方式 / 唯一」這些絕對詞</li>
<li><input disabled="" type="checkbox"> 每個絕對詞檢查：是物理 / 法律 / 安全事實嗎？不是的話翻成「在 X 情境下 A 較好、Y 情境下 B 較好」</li>
<li><input disabled="" type="checkbox"> 反模式表的「為什麼不好」寫到「違反某個原則」、不寫「就是不對」</li>
</ul>
<h3 id="輪-4grep-ability--命名">輪 4：Grep-ability / 命名</h3>
<ul>
<li><input disabled="" type="checkbox"> 關鍵字在段首、不在段中</li>
<li><input disabled="" type="checkbox"> 表格欄位用 grep 友善的分隔（<code>:</code> <code>|</code> <code>→</code>）</li>
<li><input disabled="" type="checkbox"> 檔名 / slug 跟 title 對應、不要用流水號</li>
<li><input disabled="" type="checkbox"> 命名能用單次 grep 命中、不需要語意推理</li>
</ul>
<h3 id="輪-5反例--邊界">輪 5：反例 / 邊界</h3>
<ul>
<li><input disabled="" type="checkbox"> 「何時不適用」段寫了嗎？</li>
<li><input disabled="" type="checkbox"> 反模式表給「為什麼不好 + 修法」嗎、還是只給警告？</li>
<li><input disabled="" type="checkbox"> 跨卡 cross-link 補了嗎？</li>
<li><input disabled="" type="checkbox"> 有沒有 over-claim、把「在多數情境下」說成「總是」？</li>
</ul>
<hr>
<h2 id="套用到不同-output-類型">套用到不同 output 類型</h2>
<p>每類有特化的輪次組合：</p>
<h3 id="文章contentpostscontentreport">文章（content/posts、content/report）</h3>
<p>完整跑 1-5 輪。額外加：</p>
<ul>
<li><strong>輪 6</strong>：跨卡 cross-link 健康度（單向引用 vs 雙向）</li>
<li><strong>輪 7</strong>：放回 <code>_index.md</code> 的索引條描述對應到內容嗎</li>
</ul>
<h3 id="程式註解doc-comment--inline">程式註解（doc comment / inline）</h3>
<p>跑 1-3 輪 + 額外：</p>
<ul>
<li><strong>輪 4&rsquo;</strong>：grep-ability 改成「介面 vs 實作分層」— doc comment 不洩漏 impl、inline comment 講 why 不講 what</li>
<li><strong>輪 5&rsquo;</strong>：反例改成「這個註解 5 個月後讀還看得懂嗎」（時間軸 robust）</li>
</ul>
<h3 id="naming變數--函式--檔名--slug">Naming（變數 / 函式 / 檔名 / slug）</h3>
<p>→ 見 <a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a>、有特化的 4 輪設計。</p>
<h3 id="prompt給-llm-的指令">Prompt（給 LLM 的指令）</h3>
<p>跑 1-3 輪 + 額外：</p>
<ul>
<li><strong>輪 4&rsquo;&rsquo;</strong>：模糊指令（<a href="/blog/skills/requirement-protocol/clarifying-ambiguous-instructions/" data-link-title="Clarifying Ambiguous Instructions — 模糊指令澄清協議" data-link-desc="requirement-protocol reference：空間 / 相對位置 / 隔離 / 決定權 / 篩選五類模糊指令的澄清模板 &#43; visible 三問判準 &#43; 篩選三問。">clarifying-ambiguous-instructions</a>）— 「對齊」「靠近」這類詞翻成具體數字 / 條件</li>
<li><strong>輪 5&rsquo;&rsquo;</strong>：「邊界 case 預期行為」明示了嗎</li>
</ul>
<hr>
<h2 id="stakes-conditional-追加輪epistemic-rigor">Stakes-conditional 追加輪：Epistemic Rigor</h2>
<p>5 輪基本 frame 是 frame 軸（生成 / 意圖 / 語氣 / grep / 反例）；<strong>高 stakes 內容</strong>（reader 照做後錯誤不可逆 / 系統層 / 不可分批 ship 修正）追加 stakes 軸的 <strong>輪 E：epistemic rigor</strong>——比照學術 peer review 的 claim / evidence / method / threats / citation 五個 sub-check、確認論述強度足以承擔 reader 直接 implement 的下游風險。</p>
<h3 id="啟動條件opt-in不污染預設">啟動條件（opt-in、不污染預設）</h3>
<table>
  <thead>
      <tr>
          <th>內容類型</th>
          <th>是否跑輪 E</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般技術文章（layout / refactor / debug 教學）</td>
          <td>不跑（5 輪夠）</td>
      </tr>
      <tr>
          <td>資安 / cryptography / 防護 mitigation 教學</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Concurrency 正確性 / memory model claims</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Distributed consistency / consensus 演算法</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Financial 計算 / accounting / settlement</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>Medical / safety-critical 計算</td>
          <td><strong>跑</strong></td>
      </tr>
      <tr>
          <td>任何「reader 照做後錯誤不可逆 / 系統層」的內容</td>
          <td><strong>跑</strong></td>
      </tr>
  </tbody>
</table>
<p>判別啟動的核心問題：「<strong>reader 照這段實作會不會在生產系統留 silent gap、且不能靠後續 ship 修補</strong>？」——會 → 跑輪 E、不會 → 5 輪即可。動機論證見 <a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a>。</p>
<h3 id="輪-eepistemic-rigor-的-5-個-sub-check">輪 E：Epistemic Rigor 的 5 個 sub-check</h3>
<table>
  <thead>
      <tr>
          <th>Sub-check</th>
          <th>問題</th>
          <th>對應 audit 維度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>E.1 Claim</td>
          <td>每個結論可拆 falsifiable 子句嗎？</td>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a></td>
      </tr>
      <tr>
          <td>E.2 Evidence</td>
          <td>claim → evidence 推論鏈完整、有 mechanism 嗎？</td>
          <td><a href="../mitigation-threat-alignment/">#102 mitigation 對位</a></td>
      </tr>
      <tr>
          <td>E.3 Method</td>
          <td>reader 照 method 做能反向驗證嗎？</td>
          <td><a href="../threat-model-explicitness/">#101 threat model 明確性</a> + <a href="../mitigation-threat-alignment/">#102</a></td>
      </tr>
      <tr>
          <td>E.4 Threats</td>
          <td>什麼前提失效會 invalidate？</td>
          <td><a href="../mitigation-context-dependence/">#103 context-dependence</a></td>
      </tr>
      <tr>
          <td>E.5 Citation</td>
          <td>版本 / 句意 / current best practice 都對嗎？</td>
          <td><a href="../security-citation-currency-and-precision/">#104 citation 時效精確</a></td>
      </tr>
  </tbody>
</table>
<h3 id="輪-e-的產出格式">輪 E 的產出格式</h3>
<p>跑完輪 E、每個 weakness 對應到一個 dimension + tier、見 <a href="../security-audit-recommendation-tiers/">#105 audit recommendation 層級</a>：</p>
<ul>
<li><strong>Accept</strong>：無 weakness 或在容忍範圍</li>
<li><strong>Minor revise</strong>：補 boundary / contrast / 版本標記類小改、不阻擋 ship</li>
<li><strong>Major revise</strong>：結構性 false sense、需重寫、ship 前必須修</li>
<li><strong>Withdraw</strong>：教錯主動誤導 reader（過時 crypto 沒標 deprecated / 扭曲 citation 反向違反現行標準 / defense theater 當示範），保留 = 增加生產系統 risk、必須移除或全換</li>
</ul>
<p>withdraw tier 是高 stakes 內容跟一般內容的關鍵差異——一般內容 review 沒有「保留 = 增加 risk」的硬決策、高 stakes 必須有。</p>
<h3 id="跟-5-輪基本-frame-的分工">跟 5 輪基本 frame 的分工</h3>
<table>
  <thead>
      <tr>
          <th>軸</th>
          <th>5 輪基本 frame</th>
          <th>輪 E（高 stakes 追加）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>軸定位</td>
          <td>Frame 軸：每輪換一個寫作品質視角</td>
          <td>Stakes 軸：論述強度檢查</td>
      </tr>
      <tr>
          <td>觸發</td>
          <td>預設全跑（依 output 類型可跳少數輪）</td>
          <td>高 stakes 內容才 opt-in</td>
      </tr>
      <tr>
          <td>找的問題</td>
          <td>typo / 偏題 / 絕對主義 / grep / 邊界缺</td>
          <td>claim 空降 / 對位失效 / context 缺 / citation 過時 / withdraw-level 教錯</td>
      </tr>
      <tr>
          <td>失敗後果</td>
          <td>文字品質低、reader 用力讀</td>
          <td>reader 照做後實作出生產破口、silent failure</td>
      </tr>
  </tbody>
</table>
<p>兩軸正交、不取代——高 stakes 內容兩軸都跑（5 輪 frame + 輪 E stakes）、一般內容只跑 5 輪。輪 E 不在 5 輪裡是因為：把 epistemic rigor 設為預設會讓一般文章 over-audit、稀釋 review 紀律；設為 conditional opt-in 才能讓高 stakes 場景拉到學術級而不污染日常寫作。</p>
<p>→ 詳細維度展開（threat model 對稱 / mitigation 對位 mechanism / context-dependence / citation 時效）跟 audit recommendation tier 判準、見 <a href="../security-teaching-rigor-asymmetry/">#99</a> → <a href="../false-sense-of-security-as-primary-failure/">#100</a> → <a href="../threat-model-explicitness/">#101-104</a> → <a href="../security-audit-recommendation-tiers/">#105</a> 系列。</p>
<hr>
<h2 id="反模式跳輪的代價">反模式：跳輪的代價</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫完直接 ship、跳過所有 review 輪</td>
          <td>第 1 輪生成 frame 沒抓到的全部漏</td>
      </tr>
      <tr>
          <td>每輪用同個 frame review</td>
          <td>角度沒換、重複 catch 同類錯、新類錯不會浮現</td>
      </tr>
      <tr>
          <td>「我邊寫邊改」融合多輪</td>
          <td>working memory 超載、每維都做一半</td>
      </tr>
      <tr>
          <td>跳過輪 3 機會成本語氣</td>
          <td>絕對主義教讀者規則、不教思考</td>
      </tr>
      <tr>
          <td>跳過輪 4 grep</td>
          <td>AI 找不到、文字變孤兒</td>
      </tr>
      <tr>
          <td>跳過輪 5 反例</td>
          <td>看起來是教條、不是 trade-off</td>
      </tr>
      <tr>
          <td>「下次寫的時候多注意」當 review 取代</td>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> 紀律失效</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時可以跳某些輪">何時可以跳某些輪</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>可跳的輪</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內部 quick note、不會有人看</td>
          <td>跳 4 + 5（grep + 反例）</td>
      </tr>
      <tr>
          <td>Commit message</td>
          <td>跳 4 + 5、留 1-3</td>
      </tr>
      <tr>
          <td>Slack / chat 即時對話</td>
          <td>只跑輪 1</td>
      </tr>
      <tr>
          <td>引言 / 標題</td>
          <td>1-4 都跑、5 可省</td>
      </tr>
      <tr>
          <td>摘要 / TL;DR</td>
          <td>1-3 + 5（反例不適用、但語氣很重要）</td>
      </tr>
      <tr>
          <td>純資料填寫（schema / config）</td>
          <td>跳 3、其他都跑</td>
      </tr>
  </tbody>
</table>
<p>四類共通：<strong>ROI 不同、輪次組合不同</strong>。Production 文件 / 卡片 / 註解 全跑、即時通訊只跑核心。</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>輪 2 的核心判準 — 為什麼便利寫法 ≠ 對齊意圖</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>本卡是 #82 在「寫」這個動作的具體實例 — review 是 multi-pass、不是 hook</td>
      </tr>
      <tr>
          <td><a href="../cards-as-living-system-iteration/">#81 卡片系統的迭代浮現</a></td>
          <td>spiral 浮現本身就是寫作的 multi-pass 範例（卡片 → meta → reference）</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>寫決策呈現要用 #79 self-check + 本卡的輪 2 一起跑</td>
      </tr>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>本卡的輪 4 在 naming 場景的特化</td>
      </tr>
      <tr>
          <td><a href="../methodology-multi-pass-embedding/">#85 Methodology 的 multi-pass 該 embed 在 pillar</a></td>
          <td>本卡的 5 輪設計就是 compositional-writing 該 embed 為「第 6 原則」的內容</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫完直接 commit、覺得「OK 應該夠」</td>
          <td>跑五輪、每輪都會抓到東西</td>
      </tr>
      <tr>
          <td>每次 review 都「看不出哪裡可改」</td>
          <td>Frame 沒換、改用下一輪的 frame 看</td>
      </tr>
      <tr>
          <td>「這次先這樣、下次寫好一點」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a>、補 trigger（pre-commit / template / pair）</td>
      </tr>
      <tr>
          <td>反模式段空白</td>
          <td>跳了輪 5、補</td>
      </tr>
      <tr>
          <td>找不到自己寫過的卡</td>
          <td>輪 4 沒做、grep-ability 漏掉</td>
      </tr>
      <tr>
          <td>文字看起來像教條</td>
          <td>輪 3 的絕對主義詞沒翻、補</td>
      </tr>
      <tr>
          <td>段落開頭看不出在說什麼</td>
          <td>輪 2 的意圖顯性沒做</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Writing 的 ROI 來自<strong>多輪不同 frame</strong>、不是單次「寫得仔細」。要寫得仔細的部分太多、超過 working memory、必須分輪。<strong>跳輪的代價是某維度永遠做一半、累積成「看起來都對但其實有漏」的低品質文字</strong>。</p>
]]></content:encoded></item><item><title>Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂</title><link>https://tarrragon.github.io/blog/report/naming-as-iterated-artifact/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/naming-as-iterated-artifact/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>第一次寫的名字幾乎都不對 — 不是因為命名能力不夠、是因為&lt;strong>第一版命名只能基於「寫的當下看到的 context」&lt;/strong>、而正確的名字需要看到「未來所有 call-site / grep 結果 / 重構場景」。&lt;/p>
&lt;p>命名的正確設計是 &lt;strong>iterated artifact&lt;/strong>：寫 → re-read → 改 → 再 re-read → 收斂。每輪用不同 frame：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>輪&lt;/th>
 &lt;th>Frame&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>Grep-ability&lt;/td>
 &lt;td>能單次 grep 命中嗎？跟其他 entity 名字不撞嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>Cross-call-site&lt;/td>
 &lt;td>從 caller 角度看、名字暗示的契約對嗎？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>Impl 洩漏檢查&lt;/td>
 &lt;td>名字洩漏了 impl 細節嗎？換 impl 名字會錯嗎？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每輪可能 catch 到「上一輪沒看到」的問題、迫使重命名。&lt;strong>接受重命名是命名工作的常態、不是失敗&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼第一版幾乎不對">為什麼第一版幾乎不對&lt;/h2>
&lt;p>寫第一版時、認知資源都在「概念是什麼」、剩下的給命名只夠：&lt;/p>
&lt;ul>
&lt;li>看到當前 function 在做的事 → 命名只反映當前&lt;/li>
&lt;li>不知道未來會有 N 個 call-site → 沒考慮一致性&lt;/li>
&lt;li>不知道未來會有 grep / refactor → 沒考慮 unique-ness&lt;/li>
&lt;li>不知道未來會換 impl → 命名容易洩漏現在的 impl 細節&lt;/li>
&lt;/ul>
&lt;p>第一版命名是&lt;strong>對「現在的 context」過度擬合&lt;/strong>。下一輪 review 換 frame 才能看到擬合方向之外。&lt;/p>
&lt;hr>
&lt;h2 id="四輪-review-的具體-checklist">四輪 review 的具體 checklist&lt;/h2>
&lt;h3 id="輪-1第一版">輪 1：第一版&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 名字反映「做什麼 / 是什麼」、不是「怎麼做」&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 動詞 / 名詞符合語言慣例（function 動詞、value 名詞）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 不超過 4 個單字（長 ≠ 清楚）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跑得到 next step、不在這輪糾結&lt;/li>
&lt;/ul>
&lt;h3 id="輪-2grep-ability">輪 2：Grep-ability&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;code>grep -r &amp;quot;&amp;lt;name&amp;gt;&amp;quot;&lt;/code> 能命中目標、不會被別的 entity 蓋過&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跟 framework / library reserved name 不撞（避免 &lt;code>data&lt;/code>、&lt;code>type&lt;/code>、&lt;code>value&lt;/code> 等過泛）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 名字不是其他名字的子字串（&lt;code>get&lt;/code> 會匹配 &lt;code>getName&lt;/code> &lt;code>getUser&lt;/code>&amp;hellip;）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 中英混合場景下、英文部分能 grep（不要用 &lt;code>處理器handler&lt;/code> 這種 mixed）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 縮寫慎用（&lt;code>usr&lt;/code> &lt;code>cfg&lt;/code> &lt;code>mgr&lt;/code> 增加 grep 失敗率）&lt;/li>
&lt;/ul>
&lt;h3 id="輪-3cross-call-site-一致性">輪 3：Cross-call-site 一致性&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 從 caller 角度看、名字暗示的契約對嗎？&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 跟同 module 其他類似 entity 命名格式一致嗎？（&lt;code>getUser&lt;/code> vs &lt;code>fetchUser&lt;/code> 不該混用）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 同一個概念在不同 file 用同名嗎？（不該 &lt;code>userId&lt;/code> / &lt;code>user_id&lt;/code> / &lt;code>uid&lt;/code> 三個並存）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 動詞時態一致嗎？（&lt;code>fetched&lt;/code> vs &lt;code>fetching&lt;/code> vs &lt;code>fetch&lt;/code> 對應狀態 / 動作 / 命令、不該混用）&lt;/li>
&lt;/ul>
&lt;h3 id="輪-4impl-洩漏檢查">輪 4：Impl 洩漏檢查&lt;/h3>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> 名字含 impl 細節嗎？（&lt;code>fetchUserViaSql&lt;/code> ≠ &lt;code>fetchUser&lt;/code>、後者較好）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 換 impl 後名字還對嗎？（&lt;code>cacheGetUser&lt;/code> 改成走 DB 後名字錯了）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 名字洩漏 data structure 細節嗎？（&lt;code>userArray&lt;/code> ≠ &lt;code>users&lt;/code>、後者不綁 array）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 介面層名字 vs 實作層名字區分嗎？（介面用「做什麼」、實作用「怎麼做」可加細節）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="套用到不同命名場景">套用到不同命名場景&lt;/h2>
&lt;h3 id="變數--函式">變數 / 函式&lt;/h3>
&lt;p>完整跑 1-4 輪。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>第一次寫的名字幾乎都不對 — 不是因為命名能力不夠、是因為<strong>第一版命名只能基於「寫的當下看到的 context」</strong>、而正確的名字需要看到「未來所有 call-site / grep 結果 / 重構場景」。</p>
<p>命名的正確設計是 <strong>iterated artifact</strong>：寫 → re-read → 改 → 再 re-read → 收斂。每輪用不同 frame：</p>
<table>
  <thead>
      <tr>
          <th>輪</th>
          <th>Frame</th>
          <th>抓什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>第一版</td>
          <td>把概念變字串</td>
      </tr>
      <tr>
          <td>2</td>
          <td>Grep-ability</td>
          <td>能單次 grep 命中嗎？跟其他 entity 名字不撞嗎？</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Cross-call-site</td>
          <td>從 caller 角度看、名字暗示的契約對嗎？</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Impl 洩漏檢查</td>
          <td>名字洩漏了 impl 細節嗎？換 impl 名字會錯嗎？</td>
      </tr>
  </tbody>
</table>
<p>每輪可能 catch 到「上一輪沒看到」的問題、迫使重命名。<strong>接受重命名是命名工作的常態、不是失敗</strong>。</p>
<hr>
<h2 id="為什麼第一版幾乎不對">為什麼第一版幾乎不對</h2>
<p>寫第一版時、認知資源都在「概念是什麼」、剩下的給命名只夠：</p>
<ul>
<li>看到當前 function 在做的事 → 命名只反映當前</li>
<li>不知道未來會有 N 個 call-site → 沒考慮一致性</li>
<li>不知道未來會有 grep / refactor → 沒考慮 unique-ness</li>
<li>不知道未來會換 impl → 命名容易洩漏現在的 impl 細節</li>
</ul>
<p>第一版命名是<strong>對「現在的 context」過度擬合</strong>。下一輪 review 換 frame 才能看到擬合方向之外。</p>
<hr>
<h2 id="四輪-review-的具體-checklist">四輪 review 的具體 checklist</h2>
<h3 id="輪-1第一版">輪 1：第一版</h3>
<ul>
<li><input disabled="" type="checkbox"> 名字反映「做什麼 / 是什麼」、不是「怎麼做」</li>
<li><input disabled="" type="checkbox"> 動詞 / 名詞符合語言慣例（function 動詞、value 名詞）</li>
<li><input disabled="" type="checkbox"> 不超過 4 個單字（長 ≠ 清楚）</li>
<li><input disabled="" type="checkbox"> 跑得到 next step、不在這輪糾結</li>
</ul>
<h3 id="輪-2grep-ability">輪 2：Grep-ability</h3>
<ul>
<li><input disabled="" type="checkbox"> <code>grep -r &quot;&lt;name&gt;&quot;</code> 能命中目標、不會被別的 entity 蓋過</li>
<li><input disabled="" type="checkbox"> 跟 framework / library reserved name 不撞（避免 <code>data</code>、<code>type</code>、<code>value</code> 等過泛）</li>
<li><input disabled="" type="checkbox"> 名字不是其他名字的子字串（<code>get</code> 會匹配 <code>getName</code> <code>getUser</code>&hellip;）</li>
<li><input disabled="" type="checkbox"> 中英混合場景下、英文部分能 grep（不要用 <code>處理器handler</code> 這種 mixed）</li>
<li><input disabled="" type="checkbox"> 縮寫慎用（<code>usr</code> <code>cfg</code> <code>mgr</code> 增加 grep 失敗率）</li>
</ul>
<h3 id="輪-3cross-call-site-一致性">輪 3：Cross-call-site 一致性</h3>
<ul>
<li><input disabled="" type="checkbox"> 從 caller 角度看、名字暗示的契約對嗎？</li>
<li><input disabled="" type="checkbox"> 跟同 module 其他類似 entity 命名格式一致嗎？（<code>getUser</code> vs <code>fetchUser</code> 不該混用）</li>
<li><input disabled="" type="checkbox"> 同一個概念在不同 file 用同名嗎？（不該 <code>userId</code> / <code>user_id</code> / <code>uid</code> 三個並存）</li>
<li><input disabled="" type="checkbox"> 動詞時態一致嗎？（<code>fetched</code> vs <code>fetching</code> vs <code>fetch</code> 對應狀態 / 動作 / 命令、不該混用）</li>
</ul>
<h3 id="輪-4impl-洩漏檢查">輪 4：Impl 洩漏檢查</h3>
<ul>
<li><input disabled="" type="checkbox"> 名字含 impl 細節嗎？（<code>fetchUserViaSql</code> ≠ <code>fetchUser</code>、後者較好）</li>
<li><input disabled="" type="checkbox"> 換 impl 後名字還對嗎？（<code>cacheGetUser</code> 改成走 DB 後名字錯了）</li>
<li><input disabled="" type="checkbox"> 名字洩漏 data structure 細節嗎？（<code>userArray</code> ≠ <code>users</code>、後者不綁 array）</li>
<li><input disabled="" type="checkbox"> 介面層名字 vs 實作層名字區分嗎？（介面用「做什麼」、實作用「怎麼做」可加細節）</li>
</ul>
<hr>
<h2 id="套用到不同命名場景">套用到不同命名場景</h2>
<h3 id="變數--函式">變數 / 函式</h3>
<p>完整跑 1-4 輪。</p>
<p>額外注意：</p>
<ul>
<li><strong>作用域</strong> — 越窄作用域越可短（loop counter <code>i</code>、close-up var <code>tmp</code>）；越寬作用域越要明確</li>
<li><strong>類型暗示</strong> — boolean 用 <code>is</code> / <code>has</code> / <code>should</code> 開頭</li>
</ul>
<h3 id="檔名--module">檔名 / module</h3>
<p>跑 1-4 + 加：</p>
<ul>
<li><strong>層級表達</strong> — 檔名能否反映在 directory 結構中的位置？</li>
<li><strong>避免 <code>utils</code> / <code>helpers</code> / <code>common</code></strong> — 這類是「不知該叫什麼」的訊號、強制再過一次輪 1-4</li>
</ul>
<h3 id="url-slug--route">URL slug / route</h3>
<p>跑 1-4 + 加：</p>
<ul>
<li><strong>SEO</strong> — 跟 search query 的 substring match 對齊（<a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a>）</li>
<li><strong>kebab-case 一致</strong></li>
<li><strong>不含 stop words</strong>（<code>the</code>、<code>a</code>、<code>is</code>、<code>of</code>、<code>with</code>、<code>and</code>）— 跟搜尋引擎 stemming 對齊</li>
</ul>
<h3 id="api-endpoint--db-column">API endpoint / DB column</h3>
<p>跑 1-4 + 加：</p>
<ul>
<li><strong>跨 service 一致性</strong> — 同一概念在 client / server / DB 用同名（避免 <code>user_id</code> / <code>userId</code> / <code>uid</code> 跨 layer 不一致）</li>
<li><strong>不可變更性</strong> — DB column / API endpoint 改名成本極高、輪 1-4 多跑幾次值得</li>
</ul>
<hr>
<h2 id="反模式放棄重命名">反模式：放棄重命名</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「先這樣、之後再改」</td>
          <td><a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a> — 永遠不改</td>
      </tr>
      <tr>
          <td>「重命名 PR 風險高、別做」</td>
          <td>累積成 cognitive debt、後續 onboarding / debug 成本爆炸</td>
      </tr>
      <tr>
          <td>「IDE 會自動重命名、不用想清楚」</td>
          <td>IDE 改不到 doc / commit / chat 引用</td>
      </tr>
      <tr>
          <td>用 <code>data</code> <code>value</code> <code>type</code> <code>info</code> <code>obj</code> 含糊命名</td>
          <td>grep 失敗率高、自帶 false-match</td>
      </tr>
      <tr>
          <td>用語言不一致的 <code>處理 handler</code></td>
          <td>中英混雜、grep 兩邊都失敗</td>
      </tr>
      <tr>
          <td><code>tempVar1</code> <code>tempVar2</code> 流水號</td>
          <td>看不出是什麼、純佔位</td>
      </tr>
      <tr>
          <td><code>getUserById</code> 名字洩漏 query strategy</td>
          <td>換成 cache hit 後名字錯了</td>
      </tr>
      <tr>
          <td>複數同義詞並存（<code>fetch</code> / <code>get</code> / <code>load</code> / <code>retrieve</code>）</td>
          <td>caller 不知選哪個</td>
      </tr>
      <tr>
          <td>介面命名洩漏 impl（<code>HashMapUserStore</code>）</td>
          <td>impl 換 RedisStore 後 caller 跟著改</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時可以跳輪">何時可以跳輪</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>可跳輪</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loop counter / 即時 close-up var</td>
          <td>只跑輪 1</td>
      </tr>
      <tr>
          <td>Test code 內部 helper</td>
          <td>跑輪 1 + 4</td>
      </tr>
      <tr>
          <td>Temporary script / one-off</td>
          <td>1 + 2</td>
      </tr>
      <tr>
          <td>跨 team API / DB schema</td>
          <td><strong>每輪都跑、跑兩遍</strong></td>
      </tr>
      <tr>
          <td>Public library / SDK</td>
          <td><strong>每輪都跑、跑兩遍</strong></td>
      </tr>
      <tr>
          <td>Production-facing URL / endpoint</td>
          <td><strong>不可跳、改名成本極高</strong></td>
      </tr>
  </tbody>
</table>
<p>兩極：作用域越窄越可省、跨邊界 / public 越要 multi-pass。</p>
<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>本卡是 #83 輪 4 在 naming 場景的特化</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>命名 lint（max length、case style）只擋字面、grep-ability / 一致性 / impl 洩漏靠 multi-pass review</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>第一版命名是「容易寫」、不是「對齊意圖」、需要重命名</td>
      </tr>
      <tr>
          <td><a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a></td>
          <td>URL slug 的命名要跟 search 預期匹配模式對齊</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSOT</a></td>
          <td>同概念跨 layer 用同名 = naming SSOT、不該允許多版本同義</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第一次想到的名字直接用了</td>
          <td>跑輪 2-4、預期會改</td>
      </tr>
      <tr>
          <td><code>data</code> <code>type</code> <code>value</code> <code>info</code> <code>obj</code> 出現</td>
          <td>含糊命名、強制重新命</td>
      </tr>
      <tr>
          <td><code>utils</code> / <code>helpers</code> / <code>common</code> module</td>
          <td>「不知該叫什麼」訊號、重新分類</td>
      </tr>
      <tr>
          <td>Grep 命中太多無關結果</td>
          <td>名字太短 / 太泛、重命名加 prefix</td>
      </tr>
      <tr>
          <td>Caller code 看 callsite 不知契約</td>
          <td>介面名字洩漏不夠、補強或改名</td>
      </tr>
      <tr>
          <td>重構後類型 / impl 換了名字沒換</td>
          <td>命名洩漏 impl、重命名</td>
      </tr>
      <tr>
          <td>同概念出現 ≥ 2 個名字</td>
          <td>違反 SSOT、選一個改另一個</td>
      </tr>
      <tr>
          <td>重命名 PR 被 reject「沒必要」</td>
          <td>文化沒接受 naming 是 iterated、補 reviewer education</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：命名是 <strong>iterated artifact</strong>、不是 single-shot 動作。第一版基於狹窄 context 幾乎必錯。<strong>接受 N 輪 review 跟 K 次重命名是常態</strong>、命名品質會提升一個量級。試圖一次寫對 = 第一版 ship 出去 = 後續長期付 cognitive 成本。</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>Engine 不可調時、把 transformation 移到外層</title><link>https://tarrragon.github.io/blog/report/transformation-at-outer-layer-when-engine-closed/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/transformation-at-outer-layer-when-engine-closed/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>當底層 engine（search engine、LLM、DB、compiler、framework）&lt;strong>沒開放某能力的客製 API&lt;/strong>、不該硬改 engine 內部、改在 engine 的&lt;strong>輸入層 / 外層做 transformation&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Engine 限制&lt;/th>
 &lt;th>外層 transformation&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Search engine 只 prefix match&lt;/td>
 &lt;td>Build-time emit suffix tokens（&amp;ldquo;backpressure&amp;rdquo; → 加 hidden &amp;ldquo;pressure&amp;rdquo;）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>LLM 不會 CoT reasoning&lt;/td>
 &lt;td>Prompt 層加「請逐步推理」instruction&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DB 不能 query JSON 內欄位&lt;/td>
 &lt;td>預先 denormalize 成獨立 column&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Compiler 不可調 lowering&lt;/td>
 &lt;td>Source-level macro 展開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 沒 hook 點&lt;/td>
 &lt;td>Wrapper component / proxy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API rate limit 不能調&lt;/td>
 &lt;td>Client-side batching / queueing&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：engine 不開放 = 不要硬改 engine、改改你給 engine 的輸入。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼移到外層是合理-escape">為什麼移到外層是合理 escape&lt;/h2>
&lt;p>直覺反應遇到 engine 限制是「fork engine」「升級到能調的 engine」「換 engine」 — 但這三條都成本爆炸：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Fork engine&lt;/strong>：維護 fork 成本（merge upstream changes、bug fix back-port）&lt;/li>
&lt;li>&lt;strong>升級&lt;/strong>：可能需要等好幾版、可能 breaking change、可能根本沒得升&lt;/li>
&lt;li>&lt;strong>換 engine&lt;/strong>：data migration、API 重寫、config 重學&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>外層 transformation&lt;/strong> 跳過這三條：&lt;/p>
&lt;ul>
&lt;li>不動 engine 內部&lt;/li>
&lt;li>用 engine 既有的合法 API（input、metadata、wrapper）&lt;/li>
&lt;li>升級 engine 時 transformation 通常仍兼容&lt;/li>
&lt;li>換 engine 時也常能直接搬&lt;/li>
&lt;/ul>
&lt;p>代價：&lt;/p>
&lt;ul>
&lt;li>多一層 indirection&lt;/li>
&lt;li>需要維護 transformation 邏輯&lt;/li>
&lt;li>可能有 leak（transformation 不完美、edge case 露出 engine 限制）&lt;/li>
&lt;/ul>
&lt;p>通常代價遠小於三條「動 engine」路線。&lt;/p>
&lt;hr>
&lt;h2 id="五個跨領域實例">五個跨領域實例&lt;/h2>
&lt;h3 id="1-searchsuffix-token-injection">1. Search：suffix token injection&lt;/h3>
&lt;p>Pagefind / 多數 build-time search engine 只 prefix match。要支援 substring，build-time emit 額外 hidden tokens：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 原文 --&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>backpressure handling&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- transformation 後 --&amp;gt;&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">&amp;lt;&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>backpressure handling&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">p&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">span&lt;/span> &lt;span class="na">hidden&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>pressure handling&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>grep &amp;quot;pre&amp;quot;&lt;/code> → matches &lt;code>pressure&lt;/code> prefix → finds page。&lt;/p>
&lt;h3 id="2-llmprompt-level-chain-of-thought">2. LLM：prompt-level chain-of-thought&lt;/h3>
&lt;p>LLM 不會自動 CoT。在 prompt 層加：&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;/code>&lt;/pre>&lt;/div>&lt;p>Engine 沒變、輸入變了、行為變了。&lt;/p>
&lt;h3 id="3-dbdenormalized-columns">3. DB：denormalized columns&lt;/h3>
&lt;p>PostgreSQL JSON column query 慢、但:&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">ALTER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ADD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COLUMN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_id_extracted&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">GENERATED&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ALWAYS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">data&lt;/span>&lt;span class="o">-&amp;gt;&amp;gt;&lt;/span>&lt;span class="s1">&amp;#39;user_id&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STORED&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_id_extracted&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Engine 沒變、schema 加了 generated column、query 走 index、變快。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>當底層 engine（search engine、LLM、DB、compiler、framework）<strong>沒開放某能力的客製 API</strong>、不該硬改 engine 內部、改在 engine 的<strong>輸入層 / 外層做 transformation</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Engine 限制</th>
          <th>外層 transformation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search engine 只 prefix match</td>
          <td>Build-time emit suffix tokens（&ldquo;backpressure&rdquo; → 加 hidden &ldquo;pressure&rdquo;）</td>
      </tr>
      <tr>
          <td>LLM 不會 CoT reasoning</td>
          <td>Prompt 層加「請逐步推理」instruction</td>
      </tr>
      <tr>
          <td>DB 不能 query JSON 內欄位</td>
          <td>預先 denormalize 成獨立 column</td>
      </tr>
      <tr>
          <td>Compiler 不可調 lowering</td>
          <td>Source-level macro 展開</td>
      </tr>
      <tr>
          <td>Framework 沒 hook 點</td>
          <td>Wrapper component / proxy</td>
      </tr>
      <tr>
          <td>API rate limit 不能調</td>
          <td>Client-side batching / queueing</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：engine 不開放 = 不要硬改 engine、改改你給 engine 的輸入。</p>
<hr>
<h2 id="為什麼移到外層是合理-escape">為什麼移到外層是合理 escape</h2>
<p>直覺反應遇到 engine 限制是「fork engine」「升級到能調的 engine」「換 engine」 — 但這三條都成本爆炸：</p>
<ul>
<li><strong>Fork engine</strong>：維護 fork 成本（merge upstream changes、bug fix back-port）</li>
<li><strong>升級</strong>：可能需要等好幾版、可能 breaking change、可能根本沒得升</li>
<li><strong>換 engine</strong>：data migration、API 重寫、config 重學</li>
</ul>
<p><strong>外層 transformation</strong> 跳過這三條：</p>
<ul>
<li>不動 engine 內部</li>
<li>用 engine 既有的合法 API（input、metadata、wrapper）</li>
<li>升級 engine 時 transformation 通常仍兼容</li>
<li>換 engine 時也常能直接搬</li>
</ul>
<p>代價：</p>
<ul>
<li>多一層 indirection</li>
<li>需要維護 transformation 邏輯</li>
<li>可能有 leak（transformation 不完美、edge case 露出 engine 限制）</li>
</ul>
<p>通常代價遠小於三條「動 engine」路線。</p>
<hr>
<h2 id="五個跨領域實例">五個跨領域實例</h2>
<h3 id="1-searchsuffix-token-injection">1. Search：suffix token injection</h3>
<p>Pagefind / 多數 build-time search engine 只 prefix match。要支援 substring，build-time emit 額外 hidden tokens：</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;!-- 原文 --&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>backpressure handling<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c">&lt;!-- transformation 後 --&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>backpressure handling<span class="p">&lt;/</span><span class="nt">p</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">span</span> <span class="na">hidden</span><span class="p">&gt;</span>pressure handling<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span></span></span></code></pre></div><p><code>grep &quot;pre&quot;</code> → matches <code>pressure</code> prefix → finds page。</p>
<h3 id="2-llmprompt-level-chain-of-thought">2. LLM：prompt-level chain-of-thought</h3>
<p>LLM 不會自動 CoT。在 prompt 層加：</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></code></pre></div><p>Engine 沒變、輸入變了、行為變了。</p>
<h3 id="3-dbdenormalized-columns">3. DB：denormalized columns</h3>
<p>PostgreSQL JSON column query 慢、但:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">ADD</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">user_id_extracted</span><span class="w"> </span><span class="n">UUID</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="k">data</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;user_id&#39;</span><span class="p">)</span><span class="w"> </span><span class="n">STORED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">events</span><span class="p">(</span><span class="n">user_id_extracted</span><span class="p">);</span></span></span></code></pre></div><p>Engine 沒變、schema 加了 generated column、query 走 index、變快。</p>
<h3 id="4-compilersource-level-macros">4. Compiler：source-level macros</h3>
<p>C 語言沒泛型、用 <code>#define</code> macro 模擬：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="ln">1</span><span class="cl"><span class="cp">#define SWAP(a, b, T) do { T tmp = a; a = b; b = tmp; } while(0)</span></span></span></code></pre></div><p>Compiler 沒變、source 經 preprocessor transformation、行為變了。</p>
<h3 id="5-frameworkwrapper-component">5. Framework：wrapper component</h3>
<p>React 沒 onAttach lifecycle、用 wrapper:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsx" data-lang="jsx"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">withMount</span><span class="p">(</span><span class="nx">Component</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">return</span> <span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">useEffect</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">props</span><span class="p">.</span><span class="nx">onMount</span><span class="o">?</span><span class="p">.();</span> <span class="p">},</span> <span class="p">[]);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="p">&lt;</span><span class="nt">Component</span> <span class="p">{</span><span class="na">...props</span><span class="p">}</span> <span class="p">/&gt;;</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></span></code></pre></div><p>React 沒變、加一層 wrapper、行為加上了。</p>
<hr>
<h2 id="input-跟-output-transformation-是不同-pattern">Input 跟 Output transformation 是不同 pattern</h2>
<p>外層 transformation 不是單一 pattern、要區分 <strong>input transformation</strong>（改 engine 看到的）跟 <strong>output transformation</strong>（改 engine 給出的）：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Input transformation</th>
          <th>Output transformation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>動的位置</td>
          <td>Engine 之前（給 engine 的輸入）</td>
          <td>Engine 之後（engine 的輸出）</td>
      </tr>
      <tr>
          <td>Engine 行為</td>
          <td>改變（看到不同輸入、做出不同事）</td>
          <td>不變（後處理而已）</td>
      </tr>
      <tr>
          <td>副作用</td>
          <td>可能改變 ranking / 內部狀態</td>
          <td>純後加工、對 engine 透明</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>Input 錯誤 → engine 整個跑錯</td>
          <td>Output 處理錯 → 個別 case 不對</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>改變 engine 「能看到什麼」</td>
          <td>補強 engine 「沒給的東西」</td>
      </tr>
  </tbody>
</table>
<h3 id="例對照">例對照</h3>
<p><strong>Search</strong>：</p>
<ul>
<li>Input：build-time 加 suffix tokens（engine 索引時看到 hidden &ldquo;pressure&rdquo;）</li>
<li>Output：result 出來後 client-side substring scan（engine 結果照常、額外加 fallback）</li>
</ul>
<p><strong>LLM</strong>：</p>
<ul>
<li>Input：prompt 加 &ldquo;請逐步推理&rdquo;（engine 看到不同 prompt）</li>
<li>Output：parse output 後 extract structured data（engine 輸出照常、後處理）</li>
</ul>
<p><strong>DB</strong>：</p>
<ul>
<li>Input：generated column（engine 索引時看到 denormalized column）</li>
<li>Output：query 結果後處理 / aggregation（engine 結果照常、應用層加工）</li>
</ul>
<h3 id="何時選哪個">何時選哪個</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>選</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>想改變 engine 結果的「內容覆蓋」</td>
          <td>Input</td>
      </tr>
      <tr>
          <td>Engine 不該被改、純補強 output</td>
          <td>Output</td>
      </tr>
      <tr>
          <td>副作用衝突風險高（如 search ranking）</td>
          <td>Output（更安全）</td>
      </tr>
      <tr>
          <td>需要 engine 配合（index size、build cost）</td>
          <td>Input（更徹底）</td>
      </tr>
  </tbody>
</table>
<p><strong>多數實作 case 兩者疊加</strong>：input 解 80%（更徹底）、output 補剩下 20%（catch input 漏的）。</p>
<hr>
<h2 id="何時不該用外層-transformation">何時不該用外層 transformation</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Engine 開放了 API、有官方解</td>
          <td>用官方、別自己 transformation</td>
      </tr>
      <tr>
          <td>Transformation 跟 engine 行為衝突（例：injected tokens 影響 ranking）</td>
          <td>副作用大、考慮其他路</td>
      </tr>
      <tr>
          <td>Transformation 邏輯比 engine 還複雜</td>
          <td>可能該換 engine 了</td>
      </tr>
      <tr>
          <td>Transformation 永遠 catch 不全 edge case</td>
          <td>用了會誤導、不如顯式說「不支援」（<a href="../capability-gap-three-layer-escalation/">#86 L1</a>）</td>
      </tr>
      <tr>
          <td>Engine 升級會破壞 transformation</td>
          <td>維護成本長期高</td>
      </tr>
  </tbody>
</table>
<p>五類共通：<strong>transformation 的成本 / 風險 &gt; 動 engine 的成本</strong>。其他情境外層 transformation 是首選。</p>
<hr>
<h2 id="跟-45-外部組件合作四層的關係">跟 #45 外部組件合作四層的關係</h2>
<p><a href="../external-component-collaboration-layers/">#45</a> 講「離公共介面越近越穩」、本卡是這條原則的具體展開：</p>
<table>
  <thead>
      <tr>
          <th>#45 層次</th>
          <th>本卡對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>公共介面層（最穩）</td>
          <td>Engine 開放的 API</td>
      </tr>
      <tr>
          <td>邊界層</td>
          <td><strong>外層 transformation</strong>（本卡焦點）</td>
      </tr>
      <tr>
          <td>內部結構層</td>
          <td>Engine 內部、不該動</td>
      </tr>
      <tr>
          <td>客戶端層</td>
          <td>Wrapper / proxy</td>
      </tr>
  </tbody>
</table>
<p><strong>外層 transformation 是邊界層的具體技法</strong> — 在 engine 公共介面外、做 input / output transformation。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>沒先試外層、直接 fork engine</td>
          <td>維護成本爆炸</td>
      </tr>
      <tr>
          <td>Transformation 寫得太聰明、catch 不全 case</td>
          <td>看似 work、暗藏 silent failure</td>
      </tr>
      <tr>
          <td>Transformation 跟 engine 預設行為衝突</td>
          <td>結果不可預期</td>
      </tr>
      <tr>
          <td>把 transformation 寫在 engine code 裡（混入）</td>
          <td>該升級 engine 時 transformation 跟著動、失去隔離價值</td>
      </tr>
      <tr>
          <td>Engine 升級後不重 review transformation</td>
          <td>可能新版已支援、舊 transformation 變累贅</td>
      </tr>
      <tr>
          <td>Transformation 沒文件、只有 implicit comment</td>
          <td>後人不懂為什麼 / 不敢碰</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 外部組件合作四層</a></td>
          <td>本卡是「邊界層」的具體技法</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層階梯</a></td>
          <td>外層 transformation 多是 L2 augmenting computation 的實作方式</td>
      </tr>
      <tr>
          <td><a href="../build-time-vs-runtime-computation-spectrum/">#87 Build-time vs Runtime</a></td>
          <td>Transformation 可放 build-time（suffix token）或 runtime（query rewrite）</td>
      </tr>
      <tr>
          <td><a href="../search-engine-matching-mode-mismatch/">#73 search 匹配模式</a></td>
          <td>search engine prefix-only 限制是本卡 case 1 的具體場景</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>Transformation 是字面層 catch、適合 hook 自動化（build step）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「engine 不支援 X、所以 X 不能做」</td>
          <td>檢查能不能在外層做 transformation</td>
      </tr>
      <tr>
          <td>「我們需要 fork 這個 lib」</td>
          <td>先試外層、多數情況夠</td>
      </tr>
      <tr>
          <td>「等 upstream 加 feature」</td>
          <td>多半永遠等不到、外層先解</td>
      </tr>
      <tr>
          <td>「這個 hack 太醜、要改 engine」</td>
          <td>醜不是換工具的理由、看實際 ROI</td>
      </tr>
      <tr>
          <td>Transformation 寫了沒文件</td>
          <td>補 why、否則後人會誤拆</td>
      </tr>
      <tr>
          <td>同一 engine 累積 ≥ 3 種 transformation</td>
          <td>可能該換 engine 了</td>
      </tr>
      <tr>
          <td>升級 engine 後 transformation 沒測</td>
          <td>可能新版 native 支援、舊 transformation 多餘</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Engine 限制不等於 capability 限制 — engine 沒開放的能力、通常可在 engine 的輸入 / 輸出層做 transformation 補上。<strong>「engine 不支援」是表象、「我沒思考外層解」是根因</strong>。</p>
]]></content:encoded></item><item><title>Dataset 規模改變什麼可行：「需要 index」依 scale 而定</title><link>https://tarrragon.github.io/blog/report/dataset-scale-changes-feasibility/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/dataset-scale-changes-feasibility/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>「需要 index、需要 cache、需要 hot path」這類設計動詞、預設是 &lt;strong>production scale 的語言&lt;/strong>。Dataset 小的時候、O(N) scan 甚至 O(N²) 都可能 trivial、不需要 index。&lt;/p>
&lt;p>具體 threshold：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Dataset 大小&lt;/th>
 &lt;th>可行操作&lt;/th>
 &lt;th>Index / cache 必要？&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>&amp;lt; 1 MB&lt;/strong>&lt;/td>
 &lt;td>O(N²) 全表 scan、JS regex、無腦比對&lt;/td>
 &lt;td>完全不需要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>1-10 MB&lt;/strong>&lt;/td>
 &lt;td>O(N) full scan per query、簡單 in-memory 處理&lt;/td>
 &lt;td>通常不需要、有 index 是 nice-to-have&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>10-100 MB&lt;/strong>&lt;/td>
 &lt;td>O(N) 仍可、但要避免 per-keystroke；考慮 lazy load + index&lt;/td>
 &lt;td>開始需要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>100 MB - 1 GB&lt;/strong>&lt;/td>
 &lt;td>必須 index / 分塊 / streaming&lt;/td>
 &lt;td>必要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>&amp;gt; 1 GB&lt;/strong>&lt;/td>
 &lt;td>必須分散式 / DB / search engine&lt;/td>
 &lt;td>強制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>錯誤預設&lt;/strong>：「production scale 設計」直接套用到小 dataset = 過度工程。&lt;strong>修法&lt;/strong>：先量測實際 dataset 大小、再決定要不要 index。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼預設-production-scale-是反模式">為什麼預設 production scale 是反模式&lt;/h2>
&lt;p>寫程式的習慣偏好「scalable solution」 — 但在 small dataset 情境下：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>過度工程成本&lt;/strong>：寫 / 維護 index、cache invalidation、tiered storage 都是 cost&lt;/li>
&lt;li>&lt;strong>實際收益 0&lt;/strong>：dataset 小、O(N) scan 已經 &amp;lt; 1ms、index 沒帶來感知差異&lt;/li>
&lt;li>&lt;strong>複雜度引入新 bug&lt;/strong>：cache invalidation 是出名難題、small dataset 直接 scan 反而對&lt;/li>
&lt;/ul>
&lt;p>實務上 80% 內部工具 / 個人專案 / 小型部落格的 dataset 是「&amp;lt; 10 MB」級別。為它寫 production-scale 設計 = 為不存在的問題付成本。&lt;/p>
&lt;hr>
&lt;h2 id="具體-threshold-跟可行操作">具體 threshold 跟可行操作&lt;/h2>
&lt;p>每段 dataset 大小都有「直覺以為需要、實際不需要」的對照：&lt;/p>
&lt;h3 id="-1-mb極小">&amp;lt; 1 MB（極小）&lt;/h3>
&lt;p>例：個人部落格內容 metadata、小型 config、單頁 SPA state。&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>Search index&lt;/td>
 &lt;td>&lt;code>Array.filter(text.includes)&lt;/code> 就夠&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pagination&lt;/td>
 &lt;td>全載入、CSS scroll&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Lazy load&lt;/td>
 &lt;td>一次全 fetch、&amp;lt; 100 KB 沒差&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Web Worker&lt;/td>
 &lt;td>主執行緒同步處理、&amp;lt; 1ms 不會卡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache&lt;/td>
 &lt;td>重算每次、&amp;lt; 1ms 沒差&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="1-10-mb小">1-10 MB（小）&lt;/h3>
&lt;p>例：本部落格 raw markdown（7.5 MB、135 篇）、中型 documentation site、小型 e-commerce catalog。&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>Search index 全集&lt;/td>
 &lt;td>Title-only index（更小）+ runtime substring fallback、依場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>分頁 query&lt;/td>
 &lt;td>全 fetch + client filter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Server-side rendering each request&lt;/td>
 &lt;td>Static + client interaction&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database indexing&lt;/td>
 &lt;td>In-memory map（如果 query pattern 簡單）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Background indexing&lt;/td>
 &lt;td>Build-time 一次處理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>但有時候已經值得 index — 視 query 頻率、複雜度、UX 需求而定（看 &lt;a href="../build-time-vs-runtime-computation-spectrum/">#87&lt;/a> 四軸）。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>「需要 index、需要 cache、需要 hot path」這類設計動詞、預設是 <strong>production scale 的語言</strong>。Dataset 小的時候、O(N) scan 甚至 O(N²) 都可能 trivial、不需要 index。</p>
<p>具體 threshold：</p>
<table>
  <thead>
      <tr>
          <th>Dataset 大小</th>
          <th>可行操作</th>
          <th>Index / cache 必要？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>&lt; 1 MB</strong></td>
          <td>O(N²) 全表 scan、JS regex、無腦比對</td>
          <td>完全不需要</td>
      </tr>
      <tr>
          <td><strong>1-10 MB</strong></td>
          <td>O(N) full scan per query、簡單 in-memory 處理</td>
          <td>通常不需要、有 index 是 nice-to-have</td>
      </tr>
      <tr>
          <td><strong>10-100 MB</strong></td>
          <td>O(N) 仍可、但要避免 per-keystroke；考慮 lazy load + index</td>
          <td>開始需要</td>
      </tr>
      <tr>
          <td><strong>100 MB - 1 GB</strong></td>
          <td>必須 index / 分塊 / streaming</td>
          <td>必要</td>
      </tr>
      <tr>
          <td><strong>&gt; 1 GB</strong></td>
          <td>必須分散式 / DB / search engine</td>
          <td>強制</td>
      </tr>
  </tbody>
</table>
<p><strong>錯誤預設</strong>：「production scale 設計」直接套用到小 dataset = 過度工程。<strong>修法</strong>：先量測實際 dataset 大小、再決定要不要 index。</p>
<hr>
<h2 id="為什麼預設-production-scale-是反模式">為什麼預設 production scale 是反模式</h2>
<p>寫程式的習慣偏好「scalable solution」 — 但在 small dataset 情境下：</p>
<ul>
<li><strong>過度工程成本</strong>：寫 / 維護 index、cache invalidation、tiered storage 都是 cost</li>
<li><strong>實際收益 0</strong>：dataset 小、O(N) scan 已經 &lt; 1ms、index 沒帶來感知差異</li>
<li><strong>複雜度引入新 bug</strong>：cache invalidation 是出名難題、small dataset 直接 scan 反而對</li>
</ul>
<p>實務上 80% 內部工具 / 個人專案 / 小型部落格的 dataset 是「&lt; 10 MB」級別。為它寫 production-scale 設計 = 為不存在的問題付成本。</p>
<hr>
<h2 id="具體-threshold-跟可行操作">具體 threshold 跟可行操作</h2>
<p>每段 dataset 大小都有「直覺以為需要、實際不需要」的對照：</p>
<h3 id="-1-mb極小">&lt; 1 MB（極小）</h3>
<p>例：個人部落格內容 metadata、小型 config、單頁 SPA state。</p>
<table>
  <thead>
      <tr>
          <th>直覺以為需要</th>
          <th>實際不需要、可用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search index</td>
          <td><code>Array.filter(text.includes)</code> 就夠</td>
      </tr>
      <tr>
          <td>Pagination</td>
          <td>全載入、CSS scroll</td>
      </tr>
      <tr>
          <td>Lazy load</td>
          <td>一次全 fetch、&lt; 100 KB 沒差</td>
      </tr>
      <tr>
          <td>Web Worker</td>
          <td>主執行緒同步處理、&lt; 1ms 不會卡</td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>重算每次、&lt; 1ms 沒差</td>
      </tr>
  </tbody>
</table>
<h3 id="1-10-mb小">1-10 MB（小）</h3>
<p>例：本部落格 raw markdown（7.5 MB、135 篇）、中型 documentation site、小型 e-commerce catalog。</p>
<table>
  <thead>
      <tr>
          <th>直覺以為需要</th>
          <th>實際不需要、可用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search index 全集</td>
          <td>Title-only index（更小）+ runtime substring fallback、依場景</td>
      </tr>
      <tr>
          <td>分頁 query</td>
          <td>全 fetch + client filter</td>
      </tr>
      <tr>
          <td>Server-side rendering each request</td>
          <td>Static + client interaction</td>
      </tr>
      <tr>
          <td>Database indexing</td>
          <td>In-memory map（如果 query pattern 簡單）</td>
      </tr>
      <tr>
          <td>Background indexing</td>
          <td>Build-time 一次處理</td>
      </tr>
  </tbody>
</table>
<p>但有時候已經值得 index — 視 query 頻率、複雜度、UX 需求而定（看 <a href="../build-time-vs-runtime-computation-spectrum/">#87</a> 四軸）。</p>
<h3 id="10-100-mb中">10-100 MB（中）</h3>
<p>例：中型公司內部工具的 user list、開源 project 全 repo、中型 dataset analytics。</p>
<table>
  <thead>
      <tr>
          <th>直覺以為需要</th>
          <th>實際</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Index for everything</td>
          <td>主要 query patterns 加 index、cold path runtime 仍可</td>
      </tr>
      <tr>
          <td>Aggressive pagination</td>
          <td>分頁 + filter pushdown</td>
      </tr>
      <tr>
          <td>Server-side scan</td>
          <td>通常仍可、加 cache 即可</td>
      </tr>
      <tr>
          <td>分散式處理</td>
          <td>通常單機夠</td>
      </tr>
  </tbody>
</table>
<h3 id="100-mb---1-gb中-大">100 MB - 1 GB（中-大）</h3>
<p>例：中型 SaaS 的 user data、search engine over medium corpus、ML feature store。</p>
<table>
  <thead>
      <tr>
          <th>直覺以為需要</th>
          <th>實際</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sharding</td>
          <td>通常單機 SSD / RAM 還夠、先垂直擴展</td>
      </tr>
      <tr>
          <td>Distributed system</td>
          <td>Single-node DB（PostgreSQL）+ index 多半夠</td>
      </tr>
      <tr>
          <td>Custom search engine</td>
          <td>Postgres FTS / SQLite FTS5 通常足夠</td>
      </tr>
      <tr>
          <td>Caching layer</td>
          <td>必要、但簡單 LRU 即可</td>
      </tr>
  </tbody>
</table>
<h3 id="-1-gb大">&gt; 1 GB（大）</h3>
<p>例：大型 SaaS、社交網路、search engine over web。</p>
<p>這個 scale 才真的需要 production-scale 設計：分散式、shard、index 嚴格設計、cache 多層、async pipeline。</p>
<hr>
<h2 id="我以後會長大的迷思">「我以後會長大」的迷思</h2>
<p>常見藉口：「現在 dataset 小、但以後會長大、所以該為以後設計」。</p>
<p>這個邏輯有兩個漏洞：</p>
<h3 id="漏洞-1未來成長不確定">漏洞 1：未來成長不確定</h3>
<p>多數內部工具 / 個人專案 / 中小企業 dataset <strong>不會</strong> 長到 production scale。為「以後可能爆炸」設計、多半是為不存在的未來付當下成本。</p>
<h3 id="漏洞-2成長前重-design-比現在-design-容易">漏洞 2：成長前重 design 比現在 design 容易</h3>
<p>當 dataset 真的長大、你會有：</p>
<ul>
<li>真實的 query pattern（vs 現在猜的）</li>
<li>真實的 hot spots（vs 現在猜的）</li>
<li>真實的 budget（vs 現在估的）</li>
</ul>
<p><strong>等需要時 redesign 比現在 over-design 划算</strong>。當前 simple design 也比較容易被 redesign（複雜結構難動）。</p>
<hr>
<h2 id="dataset-質的差別size-是-first-order-proxytype-是-second-order-multiplier">Dataset 質的差別：size 是 first-order proxy、type 是 second-order multiplier</h2>
<p>同樣 N MB dataset、不同 type 的處理成本差異大。size 只是 first approximation、要看 <strong>data type 跟 access pattern</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Type</th>
          <th>1 MB 行為</th>
          <th>10 MB 行為</th>
          <th>100 MB 行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Plain text</strong></td>
          <td>regex 全掃 &lt; 1ms</td>
          <td>regex 全掃 &lt; 50ms</td>
          <td>需分塊或 index</td>
      </tr>
      <tr>
          <td><strong>JSON / structured</strong></td>
          <td>parse + filter &lt; 10ms</td>
          <td>parse 開始貴、可能要 stream parse</td>
          <td>強制 stream / index</td>
      </tr>
      <tr>
          <td><strong>Image / binary</strong></td>
          <td>load 即一個壓力、無法 in-memory regex</td>
          <td>不能放 client、需 lazy load</td>
          <td>必 server-side 處理</td>
      </tr>
      <tr>
          <td><strong>Time series</strong></td>
          <td>順序敏感、不能 random access</td>
          <td>同 + 必須有 time index</td>
          <td>同 + 必須分區</td>
      </tr>
      <tr>
          <td><strong>Graph</strong></td>
          <td>traversal 成本不只 size、看連通度</td>
          <td>連通度高 → 易爆炸</td>
          <td>必須圖 DB</td>
      </tr>
  </tbody>
</table>
<h3 id="同-size-不同-type-的可行性逆轉">同 size 不同 type 的可行性逆轉</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 MB 純文字 search：trivial
</span></span><span class="line"><span class="ln">2</span><span class="cl">1 MB JSON deep filter：可能慢（parse + nested traversal）
</span></span><span class="line"><span class="ln">3</span><span class="cl">1 MB graph traversal：可能極慢（depth 深時呈指數）
</span></span><span class="line"><span class="ln">4</span><span class="cl">1 MB image：完全不能 client substring scan</span></span></code></pre></div><p><strong>判讀</strong>：先看 type、再看 size。size 算 capacity、type 決定每 byte 的成本。</p>
<h3 id="跨-type-共通的判準">跨 type 共通的判準</h3>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「dataset 才 1 MB 應該 fast」</td>
          <td>先確認 type — JSON / 圖 / binary 行為差異大</td>
      </tr>
      <tr>
          <td>Text 規模套到 image 場景</td>
          <td>size 直覺失效、重新評估</td>
      </tr>
      <tr>
          <td>Graph dataset 用 size 推斷</td>
          <td>看 connectivity 不只看 size</td>
      </tr>
      <tr>
          <td>Time series 想用 random access</td>
          <td>type 不對、改 sequential 設計</td>
      </tr>
  </tbody>
</table>
<p>「Dataset size」不是萬用尺、不同 type 要套不同 threshold。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>預設「需要 index」沒量 dataset</td>
          <td>過度工程、抽象 leak</td>
      </tr>
      <tr>
          <td>「production-grade」當設計詞、不分場景套</td>
          <td>內部工具 over-engineered</td>
      </tr>
      <tr>
          <td>Big-O 思維直接套小 dataset</td>
          <td>漏掉 constant factor、實際 O(N²) 比 O(N log N) 還快（小 N）</td>
      </tr>
      <tr>
          <td>Cache 在 dataset &lt; 1 MB 場景</td>
          <td>Cache invalidation 引入 bug、收益 0</td>
      </tr>
      <tr>
          <td>分散式設計在 single-node 夠用場景</td>
          <td>維運複雜度爆炸</td>
      </tr>
      <tr>
          <td>「scale 萬一爆」當預設</td>
          <td>機率低事件主導設計</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時應該照-production-scale-設計">何時應該照 production-scale 設計</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>已知會在 6 個月內 grow 10x+</td>
          <td>證據明確、提前 design 划算</td>
      </tr>
      <tr>
          <td>公開服務、流量不可控</td>
          <td>防爆炸、必要</td>
      </tr>
      <tr>
          <td>User-generated content（dataset size 由使用者決定）</td>
          <td>上限不可控</td>
      </tr>
      <tr>
          <td>Real-time / SLA 嚴格</td>
          <td>constant factor 也重要</td>
      </tr>
      <tr>
          <td>已經慢了、有實證</td>
          <td>Bottleneck 已浮現</td>
      </tr>
  </tbody>
</table>
<p>五類共通：<strong>有證據 dataset 會大 / 已大 / 不可控</strong>。其他情境用當前 dataset size 設計。</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>本卡是 #43 在「規模假設」維度的展現 — 不該 over-claim 規模</td>
      </tr>
      <tr>
          <td><a href="../build-time-vs-runtime-computation-spectrum/">#87 Build-time vs Runtime</a></td>
          <td>#87 的「dataset 大小」軸跟本卡同骨</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層階梯</a></td>
          <td>小 dataset 內 L2 augmenting 通常足夠、不必跳 L3</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>「寫 production-scale」比「量 dataset 再決定」容易、跟實際 ROI 反相關</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>真實 production 問題 ≥ 2 次再升級設計、不為「萬一」設計</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到「需要 index」沒先量 dataset</td>
          <td>量、可能不需要</td>
      </tr>
      <tr>
          <td>寫到「production-scale」當形容詞</td>
          <td>確認真的是 production scale、否則拿掉</td>
      </tr>
      <tr>
          <td>Cache 設計、dataset 卻 &lt; 1 MB</td>
          <td>拿掉 cache、直接重算</td>
      </tr>
      <tr>
          <td>分散式、卻是個人 project</td>
          <td>退回 single-node</td>
      </tr>
      <tr>
          <td>「以後會長大」當理由</td>
          <td>等真的長大、現在用 simple</td>
      </tr>
      <tr>
          <td>Big-O 焦慮、卻沒量實際 latency</td>
          <td>量了再決、constant factor 可能主導</td>
      </tr>
      <tr>
          <td>「lazy load 是必要」沒看 bundle size</td>
          <td>確認 bundle 真的大、&lt; 200 KB 全載入即可</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：「需要 index / cache / 分散式 / lazy load」這類動詞<strong>不是普世真理、是 production scale 的詞</strong>。預設套用到小 dataset = 過度工程。<strong>先量再決</strong>、跟「猜的 scale」對齊不如跟「實際 dataset」對齊。</p>
]]></content:encoded></item><item><title>L1 + L2 疊加時的訊號一致性：UX hint 跟自動 fallback 講的話要對齊</title><link>https://tarrragon.github.io/blog/report/layered-strategy-signal-consistency/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/layered-strategy-signal-consistency/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>把 &lt;a href="../capability-gap-three-layer-escalation/">L1 expectation alignment + L2 augmenting computation 疊加&lt;/a> 時、兩個 layer 給使用者的訊號要&lt;strong>對齊、不是 redundant 也不是 conflicting&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>兩 layer 的關係&lt;/th>
 &lt;th>使用者體驗&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Conflicting&lt;/strong>（L1 說一回事、L2 做相反事）&lt;/td>
 &lt;td>困惑、不信任系統&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Redundant&lt;/strong>（L1 講 + L2 補的是同個東西）&lt;/td>
 &lt;td>噪音、L1 hint 失去意義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Layered consistent&lt;/strong>（L1 講 capability、L2 自動補 + 訊號明示「這是 fallback」）&lt;/td>
 &lt;td>清楚、信任&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>設計三條原則：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>L2 自動補時、訊號要明示「這是 fallback、不是 primary path」&lt;/strong>&lt;/li>
&lt;li>&lt;strong>L1 hint 要承認 L2 的存在&lt;/strong>（不要假裝 L2 不存在）&lt;/li>
&lt;li>&lt;strong>使用者一直能 trace「這個結果怎麼來的」&lt;/strong>&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="為什麼疊加會打架">為什麼疊加會打架&lt;/h2>
&lt;p>L1 跟 L2 各自設計、不協調時、訊號會相互削弱：&lt;/p>
&lt;h3 id="conflicting-例search">Conflicting 例：search&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>L1 hint&lt;/td>
 &lt;td>&amp;ldquo;搜尋為前綴匹配、找 backpressure 請打 backpre&amp;rdquo;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>L2 fallback&lt;/td>
 &lt;td>自動 substring 找到 backpressure、顯示為 normal result&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User 打 &amp;ldquo;pre&amp;rdquo; → 看到 backpressure 結果 → 困惑：「不是說要打 backpre？」 → 不確定下次該怎麼搜。&lt;/p>
&lt;h3 id="redundant-例retry-with-hint">Redundant 例：retry with hint&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>L1 hint&lt;/td>
 &lt;td>&amp;ldquo;網路不穩、稍後再試&amp;rdquo;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>L2 retry&lt;/td>
 &lt;td>已經自動 retry 3 次&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User 看到 hint → 自己 manual retry → 但 system 已經在 retry → 操作冗餘 → 不確定 retry 是 user 觸發還是 system。&lt;/p>
&lt;h3 id="conflicting-例editor-stale-data">Conflicting 例：editor stale data&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>L1 banner&lt;/td>
 &lt;td>&amp;ldquo;資料同步可能延遲幾秒&amp;rdquo;&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>L2 fallback&lt;/td>
 &lt;td>Stale-while-revalidate 自動 refresh、user 沒感知&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>User 看到 banner、但每次資料其實都是 fresh（refresh 完成）→ banner 變 noise。Banner 撤掉後又會在某次 revalidation 失敗時 leak 出 stale data → 信任崩潰。&lt;/p>
&lt;hr>
&lt;h2 id="layered-consistency-的三設計原則">Layered Consistency 的三設計原則&lt;/h2>
&lt;h3 id="原則-1l2-自動補時訊號明示這是-fallback">原則 1：L2 自動補時、訊號明示「這是 fallback」&lt;/h3>
&lt;p>L2 不該無聲補強。當 L2 觸發、UI 應該標示：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Layered consistent 訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Search prefix-only + substring fallback&lt;/td>
 &lt;td>Result 上方標 &amp;ldquo;找到 substring 匹配（非標準前綴）&amp;quot;、user 知道這是 fallback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retry on transient failure&lt;/td>
 &lt;td>Spinner + &amp;ldquo;重試中（第 N 次）&amp;quot;、user 不需自己 retry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stale-while-revalidate&lt;/td>
 &lt;td>&amp;ldquo;資料約 N 秒前&amp;rdquo;、user 知道是否需要 refresh&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵：&lt;strong>「自動補但隱形」是 silent UX&lt;/strong>、跟 &lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉&lt;/a> 的「false confidence」同骨。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>把 <a href="../capability-gap-three-layer-escalation/">L1 expectation alignment + L2 augmenting computation 疊加</a> 時、兩個 layer 給使用者的訊號要<strong>對齊、不是 redundant 也不是 conflicting</strong>：</p>
<table>
  <thead>
      <tr>
          <th>兩 layer 的關係</th>
          <th>使用者體驗</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Conflicting</strong>（L1 說一回事、L2 做相反事）</td>
          <td>困惑、不信任系統</td>
      </tr>
      <tr>
          <td><strong>Redundant</strong>（L1 講 + L2 補的是同個東西）</td>
          <td>噪音、L1 hint 失去意義</td>
      </tr>
      <tr>
          <td><strong>Layered consistent</strong>（L1 講 capability、L2 自動補 + 訊號明示「這是 fallback」）</td>
          <td>清楚、信任</td>
      </tr>
  </tbody>
</table>
<p>設計三條原則：</p>
<ol>
<li><strong>L2 自動補時、訊號要明示「這是 fallback、不是 primary path」</strong></li>
<li><strong>L1 hint 要承認 L2 的存在</strong>（不要假裝 L2 不存在）</li>
<li><strong>使用者一直能 trace「這個結果怎麼來的」</strong></li>
</ol>
<hr>
<h2 id="為什麼疊加會打架">為什麼疊加會打架</h2>
<p>L1 跟 L2 各自設計、不協調時、訊號會相互削弱：</p>
<h3 id="conflicting-例search">Conflicting 例：search</h3>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hint</td>
          <td>&ldquo;搜尋為前綴匹配、找 backpressure 請打 backpre&rdquo;</td>
      </tr>
      <tr>
          <td>L2 fallback</td>
          <td>自動 substring 找到 backpressure、顯示為 normal result</td>
      </tr>
  </tbody>
</table>
<p>User 打 &ldquo;pre&rdquo; → 看到 backpressure 結果 → 困惑：「不是說要打 backpre？」 → 不確定下次該怎麼搜。</p>
<h3 id="redundant-例retry-with-hint">Redundant 例：retry with hint</h3>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hint</td>
          <td>&ldquo;網路不穩、稍後再試&rdquo;</td>
      </tr>
      <tr>
          <td>L2 retry</td>
          <td>已經自動 retry 3 次</td>
      </tr>
  </tbody>
</table>
<p>User 看到 hint → 自己 manual retry → 但 system 已經在 retry → 操作冗餘 → 不確定 retry 是 user 觸發還是 system。</p>
<h3 id="conflicting-例editor-stale-data">Conflicting 例：editor stale data</h3>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 banner</td>
          <td>&ldquo;資料同步可能延遲幾秒&rdquo;</td>
      </tr>
      <tr>
          <td>L2 fallback</td>
          <td>Stale-while-revalidate 自動 refresh、user 沒感知</td>
      </tr>
  </tbody>
</table>
<p>User 看到 banner、但每次資料其實都是 fresh（refresh 完成）→ banner 變 noise。Banner 撤掉後又會在某次 revalidation 失敗時 leak 出 stale data → 信任崩潰。</p>
<hr>
<h2 id="layered-consistency-的三設計原則">Layered Consistency 的三設計原則</h2>
<h3 id="原則-1l2-自動補時訊號明示這是-fallback">原則 1：L2 自動補時、訊號明示「這是 fallback」</h3>
<p>L2 不該無聲補強。當 L2 觸發、UI 應該標示：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Layered consistent 訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search prefix-only + substring fallback</td>
          <td>Result 上方標 &ldquo;找到 substring 匹配（非標準前綴）&quot;、user 知道這是 fallback</td>
      </tr>
      <tr>
          <td>Retry on transient failure</td>
          <td>Spinner + &ldquo;重試中（第 N 次）&quot;、user 不需自己 retry</td>
      </tr>
      <tr>
          <td>Stale-while-revalidate</td>
          <td>&ldquo;資料約 N 秒前&rdquo;、user 知道是否需要 refresh</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>「自動補但隱形」是 silent UX</strong>、跟 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 的「false confidence」同骨。</p>
<h3 id="原則-2l1-hint-要承認-l2-的存在">原則 2：L1 hint 要承認 L2 的存在</h3>
<p>L1 hint 不該假裝是「全部能做的事」：</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">壞：搜尋為前綴匹配、找 backpressure 請打 backpre
</span></span><span class="line"><span class="ln">2</span><span class="cl">好：搜尋優先前綴匹配；找不到時會 fallback 到 substring（顯示時會標示）。
</span></span><span class="line"><span class="ln">3</span><span class="cl">   想精準找 backpressure 直接打完整詞、或打 backpre。</span></span></code></pre></div><p>L1 講 capability + L2 講 fallback、合在一起 = 完整的 mental model。</p>
<h3 id="原則-3可-trace-結果怎麼來的">原則 3：可 trace 「結果怎麼來的」</h3>
<p>User 能（不必、但能）看到結果的來源層：</p>
<ul>
<li>Search result 標 &ldquo;prefix match&rdquo; / &ldquo;substring fallback&rdquo;</li>
<li>API response 標 <code>from_cache: true</code> 或 <code>freshness_seconds: 30</code></li>
<li>LLM response 標「來自 RAG retrieval / 來自 base model knowledge」</li>
</ul>
<p>可 trace ≠ 強制顯示、是「想知道時可以知道」。預設可隱藏、debug / 進階 user 可展開。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L2 隱形補強、L1 hint 沒提 L2</td>
          <td>使用者不知道有 fallback、抱怨 hint 不準</td>
      </tr>
      <tr>
          <td>L1 hint + L2 自動 retry 都顯示</td>
          <td>Redundant、user 重複動作</td>
      </tr>
      <tr>
          <td>L2 失敗時退回 L1 但訊號沒切換</td>
          <td>User 看到舊 hint、實際 system 在另一狀態</td>
      </tr>
      <tr>
          <td>「不要讓 user 看到 fallback」當原則</td>
          <td>Silent fallback 是 <a href="../visual-completion-vs-functional-completion/">#56 視覺完成 vs 功能完成</a> 的反例</td>
      </tr>
      <tr>
          <td>L1 / L2 是不同 team 設計、沒協調</td>
          <td>訊號自然衝突、需要 cross-team review</td>
      </tr>
      <tr>
          <td>Telemetry 沒分 L1 / L2 觸發比例</td>
          <td>不知道哪 layer 真的解 gap</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時-conflicting--redundant-是合理的">何時 conflicting / redundant 是合理的</h2>
<p>少數情境：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 conflicting / redundant 可接受</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 是 legal disclaimer（必要法律文字）</td>
          <td>法律要求、不能因 L2 拿掉</td>
      </tr>
      <tr>
          <td>L2 是 emergency fallback、L1 是 primary</td>
          <td>各自負責不同 case、訊號可重疊</td>
      </tr>
      <tr>
          <td>安全 critical 多重提醒</td>
          <td>重要訊號值得 redundant</td>
      </tr>
  </tbody>
</table>
<p>三類共通：<strong>訊號重複的成本 &lt; 訊號漏掉的成本</strong>。其他情境追求 layered consistent。</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 講疊加可行、本卡講疊加後 UX 訊號層怎麼設計</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層階梯</a></td>
          <td>#86 講選哪層、本卡講疊加多層時訊號</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>「使用者看到什麼」是 decision dialogue 的「呈現」維度、本卡是其特化</td>
      </tr>
      <tr>
          <td><a href="../visual-completion-vs-functional-completion/">#56 視覺完成 vs 功能完成</a></td>
          <td>Silent L2 fallback 是「視覺完成、功能不誠實」的變種</td>
      </tr>
      <tr>
          <td><a href="../pattern-honest-progress-ui/">#62 誠實進度 UI</a></td>
          <td>本卡的「fallback 訊號明示」原則跟誠實進度同骨</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>「自動補但隱形」是 false confidence 的 UX 變種</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="套用到當前-search-planning-case">套用到當前 search planning case</h2>
<p>D + C1 疊加 case：</p>
<p><strong>Bad</strong>（conflicting）：</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">D hint: &#34;搜尋為前綴匹配、找 backpressure 請打 backpre&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">C1 fallback: 打 &#34;pre&#34; 自動 substring 找到 backpressure、跟其他 prefix result 混排</span></span></code></pre></div><p><strong>Good</strong>（layered consistent）：</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">D hint: &#34;搜尋優先前綴匹配。找不到時自動 fallback 到 substring（會標示）。&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">C1 fallback UI:
</span></span><span class="line"><span class="ln">3</span><span class="cl">  - Prefix matches（標準）：[後跟前綴匹配 results]
</span></span><span class="line"><span class="ln">4</span><span class="cl">  - Substring matches（fallback）：[標示後跟 fallback results]</span></span></code></pre></div><p>User 看到的：</p>
<ul>
<li>打 &ldquo;pre&rdquo; → 立刻看到 prefix matches（如「prefetch」）</li>
<li>同頁標 &ldquo;Substring fallback&rdquo; 段、列「backpressure」等 substring 命中</li>
<li>看 hint 也知道為什麼有兩段</li>
</ul>
<p>訊號對齊、user mental model 完整。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hint 寫完才寫 L2、沒重 review L1</td>
          <td>退回重看 L1 是否承認 L2</td>
      </tr>
      <tr>
          <td>L2 自動補但 UI 看不出來</td>
          <td>加 fallback 訊號</td>
      </tr>
      <tr>
          <td>User 抱怨「hint 跟實際不一致」</td>
          <td>Layered consistency 沒做、補上</td>
      </tr>
      <tr>
          <td>L1 / L2 telemetry 沒分</td>
          <td>不知道誰實際 close gap、補</td>
      </tr>
      <tr>
          <td>Hint 越寫越長</td>
          <td>可能 L2 沒 surface、L1 在補 L2 該講的</td>
      </tr>
      <tr>
          <td>「user 看不到 fallback 比較單純」直覺</td>
          <td>Silent UX 反模式、 fallback 該明示</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：L1 + L2 疊加不是「兩個獨立 layer 各自做事」、是<strong>一個 capability gap 上的兩個訊號</strong>。訊號要對齊、否則使用者收到的 mental model 是 broken。<strong>Silent fallback 看起來簡潔、實際是 false confidence</strong>。</p>
]]></content:encoded></item><item><title>升級 trigger 的量化設計：「不夠就升 Y」需要明確的「不夠」指標</title><link>https://tarrragon.github.io/blog/report/escalation-trigger-quantification/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/escalation-trigger-quantification/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>&lt;a href="../capability-gap-three-layer-escalation/">#86 三層階梯&lt;/a> 的「先 L1、不夠升 L2、再不夠升 L3」協議、最容易失敗的點是「不夠」沒量化：&lt;/p>
&lt;ul>
&lt;li>沒指標 → 永遠覺得「再觀察一下」 → &lt;a href="../external-trigger-for-high-roi-work/">#72 結構性跳過&lt;/a>&lt;/li>
&lt;li>指標模糊 → 哪天該升、哪天不該、無共識&lt;/li>
&lt;li>指標太鬆 → 永遠不升、L1 一直撐到崩&lt;/li>
&lt;li>指標太嚴 → 一個小波動就升、過度工程&lt;/li>
&lt;/ul>
&lt;p>正確設計：&lt;strong>L1 ship 時就同步定 L2 升級的 trigger 條件&lt;/strong> — 閾值、觀測窗口、決策週期、誰負責決策。不是 ship 後再想。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼再觀察一下永遠不會升級">為什麼「再觀察一下」永遠不會升級&lt;/h2>
&lt;p>「ship L1 → 看效果 → 不夠就升 L2」這個 plan 在沒量化時、實際發生的是：&lt;/p>
&lt;ol>
&lt;li>L1 ship、everyone 開心&lt;/li>
&lt;li>偶爾有 user 抱怨、但「不知道是不是夠多」&lt;/li>
&lt;li>沒有明確 baseline、無法判斷「不夠」&lt;/li>
&lt;li>「再觀察一下」變固定回應&lt;/li>
&lt;li>半年過去、L2 沒 ship&lt;/li>
&lt;li>同類 capability gap 在第 N 個 feature 又發生&lt;/li>
&lt;li>「我們系統設計就這樣」變新 baseline&lt;/li>
&lt;/ol>
&lt;p>這是 &lt;a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發&lt;/a> 的具體 case — 升級是 L4（外部觸發）需要的工作、靠紀律失敗。&lt;/p>
&lt;hr>
&lt;h2 id="升級-trigger-的四元素">升級 trigger 的四元素&lt;/h2>
&lt;p>完整的升級 trigger 含四個元素：&lt;/p>
&lt;h3 id="1-metric量什麼">1. Metric（量什麼）&lt;/h3>
&lt;p>具體可量化的數字、不是模糊「使用者體驗」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Bad metric&lt;/th>
 &lt;th>Good metric&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Search prefix-only&lt;/td>
 &lt;td>&amp;ldquo;user 抱怨&amp;rdquo;&lt;/td>
 &lt;td>Empty result 率（query 結果為 0 的比例）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache miss&lt;/td>
 &lt;td>&amp;ldquo;感覺很慢&amp;rdquo;&lt;/td>
 &lt;td>P95 latency、cache hit ratio&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retry exhaustion&lt;/td>
 &lt;td>&amp;ldquo;偶爾失敗&amp;rdquo;&lt;/td>
 &lt;td>Retry-then-fail 率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stale data&lt;/td>
 &lt;td>&amp;ldquo;user 困惑&amp;rdquo;&lt;/td>
 &lt;td>Manual refresh 觸發率&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Metric 必須：&lt;/p>
&lt;ul>
&lt;li>數值化（有單位、有 baseline）&lt;/li>
&lt;li>自動量測（不靠 manual 收集）&lt;/li>
&lt;li>跟 capability gap 直接相關（不是 proxy 的 proxy）&lt;/li>
&lt;/ul>
&lt;h3 id="2-threshold什麼程度算不夠">2. Threshold（什麼程度算「不夠」）&lt;/h3>
&lt;p>明確閾值、寫進 plan：&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">Trigger：當 search empty result 率 &amp;gt; 15% 持續 2 週、升級 L2（C1 fallback）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Trigger：當 L2 ship 後 fallback 觸發率 &amp;gt; 30%、升級 L3（B1 build-time tokenize）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>閾值不是猜的、要 justify：&lt;/p>
&lt;ul>
&lt;li>從 baseline 推（現況 X、目標 Y、threshold = 中間某點）&lt;/li>
&lt;li>從業務 SLA 推（acceptable miss rate）&lt;/li>
&lt;li>從成本曲線推（升級成本 = 維持成本）&lt;/li>
&lt;/ul>
&lt;h3 id="3-window觀察多久">3. Window（觀察多久）&lt;/h3>
&lt;p>避免「一個 spike 就升」、也避免「永遠等」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Metric 性質&lt;/th>
 &lt;th>適合 window&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高頻 query（每天千次）&lt;/td>
 &lt;td>1-7 天&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中頻（每天百次）&lt;/td>
 &lt;td>2-4 週&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低頻（每天個位數）&lt;/td>
 &lt;td>1-3 月&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>偶發 incident&lt;/td>
 &lt;td>累積計數而非時間 window&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Window 太短 = noise 主導、太長 = 真問題拖太久。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p><a href="../capability-gap-three-layer-escalation/">#86 三層階梯</a> 的「先 L1、不夠升 L2、再不夠升 L3」協議、最容易失敗的點是「不夠」沒量化：</p>
<ul>
<li>沒指標 → 永遠覺得「再觀察一下」 → <a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a></li>
<li>指標模糊 → 哪天該升、哪天不該、無共識</li>
<li>指標太鬆 → 永遠不升、L1 一直撐到崩</li>
<li>指標太嚴 → 一個小波動就升、過度工程</li>
</ul>
<p>正確設計：<strong>L1 ship 時就同步定 L2 升級的 trigger 條件</strong> — 閾值、觀測窗口、決策週期、誰負責決策。不是 ship 後再想。</p>
<hr>
<h2 id="為什麼再觀察一下永遠不會升級">為什麼「再觀察一下」永遠不會升級</h2>
<p>「ship L1 → 看效果 → 不夠就升 L2」這個 plan 在沒量化時、實際發生的是：</p>
<ol>
<li>L1 ship、everyone 開心</li>
<li>偶爾有 user 抱怨、但「不知道是不是夠多」</li>
<li>沒有明確 baseline、無法判斷「不夠」</li>
<li>「再觀察一下」變固定回應</li>
<li>半年過去、L2 沒 ship</li>
<li>同類 capability gap 在第 N 個 feature 又發生</li>
<li>「我們系統設計就這樣」變新 baseline</li>
</ol>
<p>這是 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a> 的具體 case — 升級是 L4（外部觸發）需要的工作、靠紀律失敗。</p>
<hr>
<h2 id="升級-trigger-的四元素">升級 trigger 的四元素</h2>
<p>完整的升級 trigger 含四個元素：</p>
<h3 id="1-metric量什麼">1. Metric（量什麼）</h3>
<p>具體可量化的數字、不是模糊「使用者體驗」：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Bad metric</th>
          <th>Good metric</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search prefix-only</td>
          <td>&ldquo;user 抱怨&rdquo;</td>
          <td>Empty result 率（query 結果為 0 的比例）</td>
      </tr>
      <tr>
          <td>Cache miss</td>
          <td>&ldquo;感覺很慢&rdquo;</td>
          <td>P95 latency、cache hit ratio</td>
      </tr>
      <tr>
          <td>Retry exhaustion</td>
          <td>&ldquo;偶爾失敗&rdquo;</td>
          <td>Retry-then-fail 率</td>
      </tr>
      <tr>
          <td>Stale data</td>
          <td>&ldquo;user 困惑&rdquo;</td>
          <td>Manual refresh 觸發率</td>
      </tr>
  </tbody>
</table>
<p>Metric 必須：</p>
<ul>
<li>數值化（有單位、有 baseline）</li>
<li>自動量測（不靠 manual 收集）</li>
<li>跟 capability gap 直接相關（不是 proxy 的 proxy）</li>
</ul>
<h3 id="2-threshold什麼程度算不夠">2. Threshold（什麼程度算「不夠」）</h3>
<p>明確閾值、寫進 plan：</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">Trigger：當 search empty result 率 &gt; 15% 持續 2 週、升級 L2（C1 fallback）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Trigger：當 L2 ship 後 fallback 觸發率 &gt; 30%、升級 L3（B1 build-time tokenize）</span></span></code></pre></div><p>閾值不是猜的、要 justify：</p>
<ul>
<li>從 baseline 推（現況 X、目標 Y、threshold = 中間某點）</li>
<li>從業務 SLA 推（acceptable miss rate）</li>
<li>從成本曲線推（升級成本 = 維持成本）</li>
</ul>
<h3 id="3-window觀察多久">3. Window（觀察多久）</h3>
<p>避免「一個 spike 就升」、也避免「永遠等」：</p>
<table>
  <thead>
      <tr>
          <th>Metric 性質</th>
          <th>適合 window</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高頻 query（每天千次）</td>
          <td>1-7 天</td>
      </tr>
      <tr>
          <td>中頻（每天百次）</td>
          <td>2-4 週</td>
      </tr>
      <tr>
          <td>低頻（每天個位數）</td>
          <td>1-3 月</td>
      </tr>
      <tr>
          <td>偶發 incident</td>
          <td>累積計數而非時間 window</td>
      </tr>
  </tbody>
</table>
<p>Window 太短 = noise 主導、太長 = 真問題拖太久。</p>
<h3 id="4-decision-cadence誰何時how-決策">4. Decision cadence（誰、何時、how 決策）</h3>
<p>「達到 threshold」不該是「自動升級」、是「自動觸發 review」：</p>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發點</td>
          <td>Threshold 達到時系統自動 alert / 開 issue</td>
      </tr>
      <tr>
          <td>決策者</td>
          <td>預先指定（feature owner / tech lead）</td>
      </tr>
      <tr>
          <td>決策週期</td>
          <td>每月 review / 每 incident review</td>
      </tr>
      <tr>
          <td>決策 output</td>
          <td>&ldquo;升級 / 不升級 + 理由&rdquo;、寫進 log</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>決策動作有人擁有、有頻率</strong>、不靠「想到再看」。</p>
<hr>
<h2 id="l1-ship-時就定-trigger-的範本">L1 ship 時就定 trigger 的範本</h2>
<p>寫 L1 plan 時、同時寫：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># L1 (ship now)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">strategy</span><span class="p">:</span><span class="w"> </span><span class="l">UX hint</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">goal</span><span class="p">:</span><span class="w"> </span><span class="l">close 50%+ capability gap</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">metric</span><span class="p">:</span><span class="w"> </span><span class="l">search empty-result rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="nt">baseline</span><span class="p">:</span><span class="w"> </span><span class="m">18</span><span class="l">% (measured pre-ship)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">&lt; 12% within 4 weeks</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="nt">review</span><span class="p">:</span><span class="w"> </span><span class="l">weekly</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="c"># L2 trigger (defined now, executes later)</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="nt">trigger_metric</span><span class="p">:</span><span class="w"> </span><span class="l">empty-result rate</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="nt">trigger_threshold</span><span class="p">:</span><span class="w"> </span><span class="l">&gt; 15% for 2 consecutive weeks AFTER L1 ship</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="nt">trigger_owner</span><span class="p">:</span><span class="w"> </span><span class="l">search team</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="nt">trigger_action</span><span class="p">:</span><span class="w"> </span><span class="l">implement client-side substring fallback (C1)</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="nt">trigger_eta</span><span class="p">:</span><span class="w"> </span><span class="l">within 1 sprint of trigger firing</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="c"># L3 trigger (defined now, executes later)</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="nt">trigger_metric</span><span class="p">:</span><span class="w"> </span><span class="l">fallback hit rate (after L2 ship)</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="nt">trigger_threshold</span><span class="p">:</span><span class="w"> </span><span class="l">&gt; 30% sustained for 4 weeks</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="nt">trigger_owner</span><span class="p">:</span><span class="w"> </span><span class="l">search team</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="nt">trigger_action</span><span class="p">:</span><span class="w"> </span><span class="l">implement build-time suffix tokens (B1)</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w"></span><span class="nt">trigger_eta</span><span class="p">:</span><span class="w"> </span><span class="l">within 2 sprints of trigger firing</span></span></span></code></pre></div><p><strong>ship L1 時、L2 / L3 已經有「上膛」的 trigger</strong> — 不靠紀律、靠機制。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「ship L1、看狀況再說」沒寫 trigger</td>
          <td>永遠不升級（<a href="../external-trigger-for-high-roi-work/">#72</a>）</td>
      </tr>
      <tr>
          <td>Metric 寫「user happiness」（不可量）</td>
          <td>無法觸發</td>
      </tr>
      <tr>
          <td>Threshold 沒 baseline justify</td>
          <td>隨意設、無法防 over/under-trigger</td>
      </tr>
      <tr>
          <td>Window 不寫</td>
          <td>Spike 主導、或永遠等</td>
      </tr>
      <tr>
          <td>Trigger 沒 owner</td>
          <td>達到 threshold 沒人 act</td>
      </tr>
      <tr>
          <td>「達到 threshold = 自動升級」</td>
          <td>缺人工 review、可能 over-react</td>
      </tr>
      <tr>
          <td>達到 threshold 後決策延遲 1+ 個月</td>
          <td>Trigger 失去 timely value</td>
      </tr>
      <tr>
          <td>L1 / L2 / L3 升級 trigger 共用同 metric</td>
          <td>升級到 L2 後 L3 trigger 沒 reset</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時不需要量化-trigger">何時不需要量化 trigger</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 已知不夠（事前已有 evidence）</td>
          <td>直接 ship L2、不用 trigger</td>
      </tr>
      <tr>
          <td>L1 是 placeholder、L2 / L3 同 PR 一起 ship</td>
          <td>沒有「升級」、是分批</td>
      </tr>
      <tr>
          <td>問題範圍小（只影響 &lt; 1% user）</td>
          <td>量化成本 &gt; 收益</td>
      </tr>
      <tr>
          <td>MVP / 探索期</td>
          <td>規則還在演化、強行 trigger 可能卡死探索</td>
      </tr>
      <tr>
          <td>Internal tool、used by &lt; 10 人</td>
          <td>直接問 user、不需 metric</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="../capability-gap-three-layer-escalation/">#86 Capability gap 三層階梯</a></td>
          <td>#86 講升級階梯、本卡講升級 trigger 設計</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a></td>
          <td>沒 trigger 升級就是高 ROI 無觸發、本卡是補上 trigger 的方法</td>
      </tr>
      <tr>
          <td><a href="../incremental-shipping-criteria/">#76 分批 ship</a></td>
          <td>分批 ship 的「下輪」需要 trigger、本卡定 trigger</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>Trigger 是 ship 後 checkpoint 的具體形式</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>升級 trigger 通常是「N 次失敗」累積、跟 #42 同骨</td>
      </tr>
      <tr>
          <td><a href="../pattern-honest-progress-ui/">#62 誠實進度 UI</a></td>
          <td>Trigger metric 公開 = 誠實進度的數據版本</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="套用到當前-search-planning-case">套用到當前 search planning case</h2>
<p>D + C1 ship 時、應同步定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># D + C1 (ship together)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">strategy</span><span class="p">:</span><span class="w"> </span><span class="l">L1 UX hint + L2 title-only substring fallback</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">metric</span><span class="p">:</span><span class="w"> </span><span class="l">search empty-result rate, fallback hit rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">baseline</span><span class="p">:</span><span class="w"> </span><span class="l">TBD (instrument at ship time)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c"># B1 trigger (defined now)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="nt">trigger_metric</span><span class="p">:</span><span class="w"> </span><span class="l">fallback hit rate (C1)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="nt">trigger_threshold</span><span class="p">:</span><span class="w"> </span><span class="l">&gt; 30% sustained for 4 weeks</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">       </span><span class="l">OR full-content fallback request from user (manual signal)</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="nt">trigger_owner</span><span class="p">:</span><span class="w"> </span><span class="l">你（個人 blog 沒 team）</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="nt">trigger_action</span><span class="p">:</span><span class="w"> </span><span class="l">實作 Hugo template suffix tokens (B1)</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="nt">trigger_review_cadence</span><span class="p">:</span><span class="w"> </span><span class="l">每月 review search analytics</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c"># 降級 trigger（補強 #86）</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="nt">degrade_metric</span><span class="p">:</span><span class="w"> </span><span class="l">B1 maintenance cost / build pipeline complexity</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="nt">degrade_signal</span><span class="p">:</span><span class="w"> </span><span class="l">升級 Pagefind / Hugo 時 B1 broken 第 N 次</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="nt">degrade_action</span><span class="p">:</span><span class="w"> </span><span class="l">revisit 是否該換 search engine（換工具 vs 維 transformation）</span></span></span></code></pre></div><p><strong>Pre-ship 把 trigger 寫好</strong> = ship L1 時 L2 / L3 都「上膛」。下次 review 看數據、自動知道該不該升。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan 寫「ship 後再看」沒 trigger</td>
          <td>補 trigger</td>
      </tr>
      <tr>
          <td>「再觀察一下」第 3 次出現</td>
          <td>量化 trigger 不夠、明確閾值</td>
      </tr>
      <tr>
          <td>Metric 是「user 抱怨數」</td>
          <td>補可量化指標、別只靠 anecdote</td>
      </tr>
      <tr>
          <td>Threshold 沒 baseline 對比</td>
          <td>量現況、justify threshold</td>
      </tr>
      <tr>
          <td>達到 threshold 但沒人 act</td>
          <td>Trigger 沒 owner、補</td>
      </tr>
      <tr>
          <td>Window 太短、被 spike 觸發</td>
          <td>加 window、要求持續</td>
      </tr>
      <tr>
          <td>L1 ship 後沒重看 trigger</td>
          <td>設 cadence、定期 review</td>
      </tr>
      <tr>
          <td>「達到 trigger 太久才執行」</td>
          <td>ETA 沒寫、補</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：升級 trigger 的設計<strong>跟 ship plan 同步寫、不是 ship 後才想</strong>。沒 trigger = 不會升級 = capability gap 永遠在 L1 撐住。<strong>「再觀察一下」是缺 trigger 的訊號、不是「我謹慎」的訊號</strong>。</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>URL slug 必須顯式定義為 fact：跨工具 identifier 用單一定義源</title><link>https://tarrragon.github.io/blog/report/url-slug-must-be-explicit-fact/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/url-slug-must-be-explicit-fact/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>跨工具共用的 identifier（URL slug、API endpoint、route name、檔案 ID）必須&lt;strong>顯式定義在一處 fact&lt;/strong>、不能依賴各工具各自推導。多工具各自推導 = 推導鏈分歧 = silent 失敗（compile / lint 時看不出、跨工具接縫時才爆）。&lt;/p>
&lt;p>具體到 Hugo blog 的 URL slug：&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>Hugo 自動推導&lt;/td>
 &lt;td>&lt;code>title&lt;/code> 經 &lt;code>urlize&lt;/code>&lt;/td>
 &lt;td>runtime build&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>mdtools 字面比對&lt;/td>
 &lt;td>檔名（不含 &lt;code>.md&lt;/code>）&lt;/td>
 &lt;td>pre-commit&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;code>[link](/posts/X/)&lt;/code> 時、X 應該是哪個？沒有 single source 給答案。&lt;/p>
&lt;p>&lt;strong>修法&lt;/strong>：把 slug 從 derivation 升級成 fact — 在 frontmatter 顯式定義 &lt;code>slug: &amp;lt;name&amp;gt;&lt;/code>、跟檔名對齊、所有工具基於此 fact 運作、跨檔連結用此 slug。&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>Hugo&lt;/td>
 &lt;td>title → slug 推導&lt;/td>
 &lt;td>不寫 slug 也能 build、降低門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>mdtools&lt;/td>
 &lt;td>檔名 = slug&lt;/td>
 &lt;td>字面 lint、不執行 hugo runtime&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;strong>沒有單一真相&lt;/strong>」的狀態。&lt;/p>
&lt;h3 id="hugo-的-urlize-不是純函式">Hugo 的 &lt;code>urlize&lt;/code> 不是純函式&lt;/h3>
&lt;p>Hugo 的 title → slug 推導用 &lt;code>urlize&lt;/code>、規則隨版本演進、對中文 / 全形字元 / 連字符的處理會變。寫的當下推導出來的 slug、未來 hugo 升級後可能變不同 — 這是「&lt;strong>runtime 推導 = 隱性依賴 hugo 版本&lt;/strong>」。&lt;/p>
&lt;p>而 frontmatter 的 &lt;code>slug: &amp;lt;value&amp;gt;&lt;/code> 是字面值、不依賴任何工具的推導邏輯、跨版本穩定。&lt;/p>
&lt;h3 id="能-build-就不寫是便利驅動偏移67">「能 build 就不寫」是便利驅動偏移（&lt;a href="../ease-of-writing-vs-intent-alignment/">#67&lt;/a>）&lt;/h3>
&lt;p>Hugo 不寫 slug 也能 build — 寫作的當下、加 slug 是「多餘工作」、看起來沒收益。便利驅動讓寫作者跳過。但這個便利是&lt;strong>借用未來的成本&lt;/strong> — 跨檔連結時、slug 推導不一致才暴露、那時要付的修復成本遠大於當初寫 slug 的成本。&lt;/p>
&lt;hr>
&lt;h2 id="fact-vs-derivationslug-該是哪一種">Fact vs Derivation：slug 該是哪一種&lt;/h2>
&lt;p>呼應 &lt;a href="../single-source-of-truth/">#44&lt;/a> 的區分：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>定義&lt;/th>
 &lt;th>slug 的歸類&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>slug 應屬此類&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Derivation&lt;/strong>&lt;/td>
 &lt;td>從 fact 計算得出、無自主性&lt;/td>
 &lt;td>slug 不該屬此類&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>slug 必須是 fact&lt;/strong>、不是 derivation。理由：&lt;/p>
&lt;ul>
&lt;li>slug 的「值」是設計選擇 — 用什麼字串作為 URL 一部分、是 SEO / 可讀性 / 穩定性的決定、不該被自動推導左右&lt;/li>
&lt;li>一旦固定後就&lt;strong>不能改&lt;/strong>（改 slug = URL 改 = 外部連結全部死）&lt;/li>
&lt;li>「不能改」+「設計決定」= 應該是 fact&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>hugo 的 title→slug 推導&lt;/strong>：把一個 fact 偽裝成 derivation。表面上看「我只寫 title、slug 自動算出來」、實際上推導出來的 slug 變成了一個&lt;strong>新的 fact&lt;/strong>（一旦發布就不能改）、但這個 fact 的住址不在程式碼裡、在 hugo runtime 裡。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>跨工具共用的 identifier（URL slug、API endpoint、route name、檔案 ID）必須<strong>顯式定義在一處 fact</strong>、不能依賴各工具各自推導。多工具各自推導 = 推導鏈分歧 = silent 失敗（compile / lint 時看不出、跨工具接縫時才爆）。</p>
<p>具體到 Hugo blog 的 URL slug：</p>
<table>
  <thead>
      <tr>
          <th>推導鏈</th>
          <th>來源</th>
          <th>觸發時機</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hugo 自動推導</td>
          <td><code>title</code> 經 <code>urlize</code></td>
          <td>runtime build</td>
      </tr>
      <tr>
          <td>mdtools 字面比對</td>
          <td>檔名（不含 <code>.md</code>）</td>
          <td>pre-commit</td>
      </tr>
      <tr>
          <td>跨檔連結時的引用值</td>
          <td>寫作者手動算 / 複製</td>
          <td>寫作時</td>
      </tr>
  </tbody>
</table>
<p>三個推導鏈 — 寫作者寫 <code>[link](/posts/X/)</code> 時、X 應該是哪個？沒有 single source 給答案。</p>
<p><strong>修法</strong>：把 slug 從 derivation 升級成 fact — 在 frontmatter 顯式定義 <code>slug: &lt;name&gt;</code>、跟檔名對齊、所有工具基於此 fact 運作、跨檔連結用此 slug。</p>
<hr>
<h2 id="為什麼會散落">為什麼會散落</h2>
<h3 id="各工具的預設行為都合理但不一致">各工具的預設行為都「合理但不一致」</h3>
<p>每個工具在自己的領域內都做了「合理的決定」、合起來才產生不一致：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>推導決定</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hugo</td>
          <td>title → slug 推導</td>
          <td>不寫 slug 也能 build、降低門檻</td>
      </tr>
      <tr>
          <td>mdtools</td>
          <td>檔名 = slug</td>
          <td>字面 lint、不執行 hugo runtime</td>
      </tr>
      <tr>
          <td>寫作者</td>
          <td>看心情寫</td>
          <td>沒規範就靠記憶 / 複製貼上</td>
      </tr>
  </tbody>
</table>
<p>每個決定本身沒錯、合起來形成「<strong>沒有單一真相</strong>」的狀態。</p>
<h3 id="hugo-的-urlize-不是純函式">Hugo 的 <code>urlize</code> 不是純函式</h3>
<p>Hugo 的 title → slug 推導用 <code>urlize</code>、規則隨版本演進、對中文 / 全形字元 / 連字符的處理會變。寫的當下推導出來的 slug、未來 hugo 升級後可能變不同 — 這是「<strong>runtime 推導 = 隱性依賴 hugo 版本</strong>」。</p>
<p>而 frontmatter 的 <code>slug: &lt;value&gt;</code> 是字面值、不依賴任何工具的推導邏輯、跨版本穩定。</p>
<h3 id="能-build-就不寫是便利驅動偏移67">「能 build 就不寫」是便利驅動偏移（<a href="../ease-of-writing-vs-intent-alignment/">#67</a>）</h3>
<p>Hugo 不寫 slug 也能 build — 寫作的當下、加 slug 是「多餘工作」、看起來沒收益。便利驅動讓寫作者跳過。但這個便利是<strong>借用未來的成本</strong> — 跨檔連結時、slug 推導不一致才暴露、那時要付的修復成本遠大於當初寫 slug 的成本。</p>
<hr>
<h2 id="fact-vs-derivationslug-該是哪一種">Fact vs Derivation：slug 該是哪一種</h2>
<p>呼應 <a href="../single-source-of-truth/">#44</a> 的區分：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>定義</th>
          <th>slug 的歸類</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Fact</strong></td>
          <td>設計決定、不能從別處算出</td>
          <td>slug 應屬此類</td>
      </tr>
      <tr>
          <td><strong>Derivation</strong></td>
          <td>從 fact 計算得出、無自主性</td>
          <td>slug 不該屬此類</td>
      </tr>
  </tbody>
</table>
<p><strong>slug 必須是 fact</strong>、不是 derivation。理由：</p>
<ul>
<li>slug 的「值」是設計選擇 — 用什麼字串作為 URL 一部分、是 SEO / 可讀性 / 穩定性的決定、不該被自動推導左右</li>
<li>一旦固定後就<strong>不能改</strong>（改 slug = URL 改 = 外部連結全部死）</li>
<li>「不能改」+「設計決定」= 應該是 fact</li>
</ul>
<p><strong>hugo 的 title→slug 推導</strong>：把一個 fact 偽裝成 derivation。表面上看「我只寫 title、slug 自動算出來」、實際上推導出來的 slug 變成了一個<strong>新的 fact</strong>（一旦發布就不能改）、但這個 fact 的住址不在程式碼裡、在 hugo runtime 裡。</p>
<hr>
<h2 id="反模式分散的-derivation-鏈">反模式：分散的 derivation 鏈</h2>
<h3 id="多工具各自推導--silent-不一致">多工具各自推導 = silent 不一致</h3>
<p>當多個工具各自從不同 source derive 同一個 identifier、寫的當下都通過、跨工具接縫時才爆：</p>
<table>
  <thead>
      <tr>
          <th>工具 X 看到</th>
          <th>工具 Y 看到</th>
          <th>看到時機</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一致</td>
          <td>一致</td>
          <td>寫作時</td>
          <td>表面 OK、累積債</td>
      </tr>
      <tr>
          <td>一致</td>
          <td>不一致</td>
          <td>跨工具時</td>
          <td>broken link / build fail</td>
      </tr>
      <tr>
          <td>不一致</td>
          <td>不一致</td>
          <td>多版本時</td>
          <td>升級後新舊推導規則不一致</td>
      </tr>
  </tbody>
</table>
<p><strong>寫的當下看不出</strong>、是這個反模式的核心難處。</p>
<h3 id="規則膨脹誘惑教-mdtools-認-hugo-規則">「規則膨脹」誘惑：教 mdtools 認 hugo 規則</h3>
<p>碰到 mdtools 不認 hugo title 推導時、直覺反應是「教 mdtools 也跑 urlize」。這是<strong>用字面工具模擬行為層</strong>（<a href="../literal-interception-vs-behavioral-refinement/">#82</a> 的反模式）：</p>
<ul>
<li>mdtools 是字面 lint、學會 urlize → 增加實作成本、要追 hugo 版本變動</li>
<li>解決了表面症狀、但根因（slug 是 derivation）沒動</li>
<li>下一個工具（如 search index）加進來、又要再學一次 urlize</li>
</ul>
<p>正解是<strong>消滅 derivation 鏈、把 slug 升成 fact</strong>。每個工具直接讀 fact、不需要學別人的推導規則。</p>
<h3 id="之後再補-slug的-trigger-缺失">「之後再補 slug」的 trigger 缺失</h3>
<p>「先這樣、之後系統性 backfill」是 <a href="../external-trigger-for-high-roi-work/">#72</a> 的典型訊號。沒有 trigger 時、debt 永遠累積：</p>
<ul>
<li>175 篇文章沒 slug、每多寫一篇 debt 多一份</li>
<li>backfill 沒被排上 → 永遠不做</li>
<li>直到某天有人引用中文 title 的文章、broken link 才浮現</li>
</ul>
<p>修法：lint 規則加 <code>missing-slug</code> check、把 trigger 結構性建立（<a href="../escalation-trigger-quantification/">#91</a> 量化 trigger 設計）。</p>
<hr>
<h2 id="修法兩層補強">修法：兩層補強</h2>
<h3 id="規範層解根因">規範層（解根因）</h3>
<p>每篇 content 文章 frontmatter 必須有 <code>slug</code>、值跟檔名對齊（不含副檔名）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Hugo 部落格支援 Mermaid 流程圖完整實現指南&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="nt">slug</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;mermaid-gitgraph&#34;</span><span class="w">   </span><span class="c"># 跟檔名 mermaid-gitgraph.md 對齊</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2025-10-08</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="nn">---</span></span></span></code></pre></div><p>寫好後：</p>
<ul>
<li>Hugo 用 <code>slug:</code> 不再 derive title</li>
<li>mdtools 用檔名比對 frontmatter slug、字面對齊就過</li>
<li>跨檔連結 <code>[...](/posts/mermaid-gitgraph/)</code> 直接基於 slug、不需推算</li>
<li>SSoT 集中在 frontmatter、檔名是 mirror（自動驗證一致性）</li>
</ul>
<h3 id="工具層防呆">工具層（防呆）</h3>
<p>mdtools 加 lint 規則：</p>
<table>
  <thead>
      <tr>
          <th>規則 ID</th>
          <th>檢查</th>
          <th>error / warn</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1-missing-slug</td>
          <td>content 文章 frontmatter 缺 slug</td>
          <td>error（強制）</td>
      </tr>
      <tr>
          <td>L1-slug-filename-mismatch</td>
          <td>slug != 檔名 stem</td>
          <td>error</td>
      </tr>
      <tr>
          <td>L2-broken-internal-link</td>
          <td><code>/posts/&lt;slug&gt;/</code> slug 不存在</td>
          <td>error（既有）</td>
      </tr>
  </tbody>
</table>
<p>把問題從「跨檔 link 時 broken」提前到「寫作時就 catch」。</p>
<h3 id="歷史-backfill">歷史 backfill</h3>
<p>175 篇沒 slug 的文章需要 backfill。可寫 script：</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="k">for</span> f in content/posts/*.md content/work-log/*.md content/record/*.md content/report/*.md<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nv">slug</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span> .md<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> ! grep -q <span class="s2">&#34;^slug:&#34;</span> <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1"># 在 date: 後插入 slug: &lt;檔名&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    sed -i.bak <span class="s2">&#34;/^date:/a\\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s2">slug: \&#34;</span><span class="nv">$slug</span><span class="s2">\&#34;&#34;</span> <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>人工 review 確認 slug 沒衝突、commit。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../single-source-of-truth/">#44 Single Source of Truth</a></td>
          <td><strong>本卡是 #44 在 identifier 維度的具體實例</strong> — slug 散落三處、fact 升級為主修法</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>mdtools 是字面 lint、hugo urlize 是 runtime 行為 — 兩層之間的 gap 用「教字面學行為」解 = 規則膨脹、正解是消除 derivation</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>「能 build 就不寫 slug」是便利寫法、「顯式寫 slug」是對齊意圖（不依賴推導）</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a></td>
          <td>「之後系統性補 slug」沒 trigger = 永遠不會做、175 篇累積債就是這條訊號</td>
      </tr>
      <tr>
          <td><a href="../escalation-trigger-quantification/">#91 升級 trigger 的量化設計</a></td>
          <td>補 lint 規則是 trigger、把「應該補 slug」從紀律升級成結構性檢查</td>
      </tr>
      <tr>
          <td><a href="../visual-tool-error-layer-alignment/">#92 視覺手段對齊錯誤層次</a></td>
          <td>同骨：工具的 ceiling（mdtools 字面 vs hugo runtime）超出就 false confidence</td>
      </tr>
  </tbody>
</table>
<p>本卡跟 #82 / #92 共同形成「<strong>工具 ceiling pattern 系列</strong>」 — 每個工具都有能擋的層 / 擋不到的層、跨層之間需要「升級 fact」或「換工具」、不是「教工具學別人的規則」。</p>
<hr>
<h2 id="套用到本系統的-case">套用到本系統的 case</h2>
<h3 id="case-1175-篇-0-slug-的累積債">Case 1：175 篇 0 slug 的累積債</h3>
<p>實證資料（2026-04-28 撤查）：</p>
<table>
  <thead>
      <tr>
          <th>資料夾</th>
          <th>文章數</th>
          <th>有 frontmatter slug</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>posts/</td>
          <td>17</td>
          <td>0</td>
      </tr>
      <tr>
          <td>work-log/</td>
          <td>12</td>
          <td>0</td>
      </tr>
      <tr>
          <td>record/</td>
          <td>53</td>
          <td>0</td>
      </tr>
      <tr>
          <td>report/</td>
          <td>93</td>
          <td>0</td>
      </tr>
      <tr>
          <td><strong>合計</strong></td>
          <td>175</td>
          <td><strong>0</strong></td>
      </tr>
  </tbody>
</table>
<p>每一篇都是潛在的 broken link 觸發點、debt 未爆出來只因為「英文檔名跟 hugo 推導剛好一樣」。</p>
<h3 id="case-2mermaid-流程圖文章的引用-broken">Case 2：mermaid 流程圖文章的引用 broken</h3>
<p>寫 <a href="../visual-tool-error-layer-alignment/">#92</a> 的 case 2 提到 <code>mermaid_gitgraph_type_color_config</code> 文章、想連到既有的 <code>mermaid流程圖.md</code>。實際軌跡：</p>
<ol>
<li>第一直覺：寫 <code>[...](/posts/hugo-部落格支援-mermaid-流程圖完整實現指南/)</code>（hugo 推導出來的 URL）</li>
<li>mdtools L1-broken-link 失敗、它認檔名 <code>mermaid流程圖.md</code></li>
<li>改寫 <code>[...](/posts/mermaid流程圖/)</code>、hugo build 後變 404（因為 hugo 認 title 推導的 slug）</li>
<li>退而求其次：去掉超連結、改純文字提及</li>
</ol>
<p>問題的根本是「mermaid流程圖.md 沒寫 slug」 — fact 缺失、就只能在「mdtools 認的字面」跟「hugo 認的推導」中二選一、兩者都不對。</p>
<p>正解：給 mermaid流程圖.md 補 <code>slug: mermaid-gitgraph</code> 或類似、檔名 rename 對齊、所有工具基於同一 fact。</p>
<h3 id="case-3跨工具-identifier-的通用-pattern">Case 3：跨工具 identifier 的通用 pattern</h3>
<p>不只是 hugo blog — 任何「多工具共用 identifier」的情境都同樣 pattern：</p>
<table>
  <thead>
      <tr>
          <th>領域</th>
          <th>identifier</th>
          <th>散落的推導鏈</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hugo blog</td>
          <td>URL slug</td>
          <td>檔名 / hugo title / frontmatter</td>
      </tr>
      <tr>
          <td>API server</td>
          <td>endpoint route</td>
          <td>controller path / OpenAPI spec / client SDK</td>
      </tr>
      <tr>
          <td>DB migration</td>
          <td>migration ID</td>
          <td>檔名 / hash / sequence</td>
      </tr>
      <tr>
          <td>Frontend route</td>
          <td>path identifier</td>
          <td>檔案位置 / route config / navigation</td>
      </tr>
      <tr>
          <td>LLM tool name</td>
          <td>tool 名稱</td>
          <td>function name / schema / prompt 引用</td>
      </tr>
  </tbody>
</table>
<p>每一類的修法都一樣：<strong>把 identifier 升成 fact、所有工具基於此 fact</strong>、不要讓各工具各自推導。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「這個 link 為什麼 broken」debug 半天</td>
          <td>推導鏈不一致、檢查 identifier 有沒有顯式 fact</td>
      </tr>
      <tr>
          <td>「教這個工具認另一個工具的規則」</td>
          <td>規則膨脹的開始、正解是消除 derivation</td>
      </tr>
      <tr>
          <td>「能跑就不寫 X 欄位」</td>
          <td>便利驅動、未來會在跨工具接縫爆</td>
      </tr>
      <tr>
          <td>「之後系統性補 backfill」</td>
          <td><a href="../external-trigger-for-high-roi-work/">#72</a> 缺 trigger、會永遠跳過</td>
      </tr>
      <tr>
          <td>兩個工具對同個 ID 算出不同值</td>
          <td>多源 derivation、改成單一 fact</td>
      </tr>
      <tr>
          <td>升級工具版本後 link / route 全壞</td>
          <td>依賴 runtime 推導、推導規則隨版本變</td>
      </tr>
      <tr>
          <td>「我手動算一下這個 slug 應該是什麼」</td>
          <td>identifier 不該需要心算、補 fact</td>
      </tr>
      <tr>
          <td>Lint 不報錯但 production broken</td>
          <td>字面 lint 跟 runtime 行為的 gap、補 lint 規則或補 fact</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：跨工具共用的 identifier 必須是 fact、不是 derivation。<strong>Derivation 鏈把單一值散落在多工具的推導邏輯裡、寫的當下看不出問題、跨工具接縫才爆 — 而那時候 debt 已經累積到難以集中修</strong>。Fact 升級的成本（每篇加一行 frontmatter）遠小於 derivation 鏈失敗的修復成本（broken link / SEO 損失 / debugging 時間）。</p>
]]></content:encoded></item><item><title>正向改寫要保留對照論據、不能空降結論</title><link>https://tarrragon.github.io/blog/report/positive-rewrite-preserves-contrast/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/positive-rewrite-preserves-contrast/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>「X、不是 Y」這個句型在做兩件事：給結論 X、排除讀者直覺會想到的 Y。&lt;/strong> 兩者是同一個推理單元、一起出現才完整。為了滿足「正向陳述優先」而把 Y 直接刪掉、剩下的 X 就失去 contrast、變成沒依據的空降斷言。&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>保留「X、不是 Y」&lt;/td>
 &lt;td>站得住、Y 提供 contrast、讀者知道 X 排除了什麼可能性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補理由：「X、因為 …」&lt;/td>
 &lt;td>站得住、用 reasoning 取代 contrast&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>改對照表：A 欄列 X / B 欄列 Y&lt;/td>
 &lt;td>站得住、把對照升級成結構化呈現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>直接刪 Y、只剩 X&lt;/td>
 &lt;td>不站得住、X 變成空降斷言、讀者無法判斷依據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補一個更弱的 hint（「Y 只是表象」）&lt;/td>
 &lt;td>不站得住、hint 沒解釋為什麼是表象、等同沒寫&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>寫作規範要求「正向陳述優先」、AI 或作者批量改寫舊文時、為了滿足 grep &lt;code>不是 / 不要 / 避免&lt;/code> 等掃描規則、會把句子裡的否定詞機械式刪掉。但很多否定詞承擔的是 &lt;strong>contrast 角色&lt;/strong>、不是冗餘 — 抽掉之後句子在語法上正向了、但語意上失去了推理依據。&lt;/p>
&lt;p>具體例子（本 blog &lt;code>compositional-writing&lt;/code> skill 的修正過程）：&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">改 1：拆分依據是**認知負擔與情境匹配度**（行數只是表象）。
&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;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>原文：給結論（認知負擔）+ 排除讀者直覺（行數）。Y = 行數是讀者最容易想到的替代答案、明確排除掉這條路才有意義。&lt;/li>
&lt;li>改 1：把「不是 Y」軟化成「Y 只是表象」、但&lt;strong>沒解釋為什麼是表象&lt;/strong>。讀者讀完不知道「行數為什麼不算依據」、結論依然空降、只是包裝成正向句。&lt;/li>
&lt;li>改 2：直接刪、結論完全失去 contrast。&lt;/li>
&lt;/ul>
&lt;p>兩個改法都比原文差。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>正向改寫時、先判斷被刪掉的 Y 是哪一類：&lt;/p>
&lt;h3 id="y-是讀者直覺會想到的替代方案--保留-contrast">Y 是讀者直覺會想到的替代方案 → 保留 contrast&lt;/h3>
&lt;p>讀者第一直覺會想到 Y、只給 X 不夠。處理方式有三：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>保留「X、不是 Y」原句&lt;/strong>（規則六精神：反例段落要有正向錨點、但反例本身仍存在）— 這不違反「正向陳述優先」、因為「正向陳述優先」要的是「主句承載結論、反例只做對照」、不是「全段沒有否定詞」。「X、不是 Y」這個句型主句仍是 X（正向陳述）、Y 是子句裡的 contrast、結構上符合規範&lt;/li>
&lt;li>&lt;strong>補解釋&lt;/strong>：「X、因為 [推理]」 — 用 reasoning 接管 contrast 的角色&lt;/li>
&lt;li>&lt;strong>升級成對照表&lt;/strong>：兩欄並列 X 跟 Y、讓 contrast 變結構化呈現、不靠句法&lt;/li>
&lt;/ol>
&lt;h3 id="y-是冗餘否定讀者不會想到-y--直接刪">Y 是冗餘否定（讀者不會想到 Y） → 直接刪&lt;/h3>
&lt;p>例：「使用 LRU cache、不是普通 dict」— 若上下文沒有人會把 dict 當預設、Y 是冗餘、刪掉沒損失。&lt;/p>
&lt;p>判別問題：「&lt;strong>讀者讀到 X 之前、心裡最可能想到的答案是 Y 嗎？&lt;/strong> 是 → 保留 contrast。否 → 安全刪。」&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="結論變空降斷言讀者無法驗證">結論變空降斷言、讀者無法驗證&lt;/h3>
&lt;p>「行數只是表象」這種句子、讀者讀完只能選兩條路：&lt;/p>
&lt;ul>
&lt;li>全盤接受（迷信權威）&lt;/li>
&lt;li>自己腦補理由（每個讀者腦補不同、原則無法統一傳遞）&lt;/li>
&lt;/ul>
&lt;p>兩條都讓「教學」失效 — 教學的目的是讓讀者拿到 reasoning、能套用到新情境、不是讓讀者背規則。&lt;/p>
&lt;h3 id="規則崩潰壓力下會回退到刪掉的-y">規則崩潰：壓力下會回退到刪掉的 Y&lt;/h3>
&lt;p>機會成本語氣的核心 reasoning 是「絕對主義教讀者規則（壓力下會忘）、機會成本教讀者思考方式」。空降斷言是更糟的「規則」— 連「為什麼是規則」都沒給、讀者壓力下完全無錨點、會回退到自己的直覺（也就是被刪掉的 Y）。&lt;/p>
&lt;h3 id="批量改寫無法被-review">批量改寫無法被 review&lt;/h3>
&lt;p>改寫成「（Y 只是表象）」「（Y 是次級訊號）」這類軟化措辭時、表面看起來完成正向化、實際上把問題藏起來 — 規範掃描器（grep &lt;code>不是&lt;/code>）不再 catch、人類 reviewer 也容易放過、議題在文章裡持續腐爛。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/strong>：本卡是 #67 在「寫作規範執行」層的具體案例。批量正向化時、最便利的做法（grep + 機械刪否定詞）跟意圖對齊（保留推理鍊）反向 — 越容易執行、越容易產出空降斷言。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精修&lt;/a>&lt;/strong>：本卡是 #82 在「寫作規則」層的同形 pattern。grep &lt;code>不是 / 不要&lt;/code> 是字面層攔截、看不到那個「不是」承擔的是 contrast 還是冗餘 — 需要 behavioral pass（讀者能否驗證結論）。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../writing-multi-pass-review/">#83 寫作的 multi-pass review&lt;/a>&lt;/strong>：本卡補強 #83 的 frame 設計 — 「正向陳述」這輪掃完之後、要再加一輪「結論可驗證性」掃描、確認被改的句子仍然有推理依據。&lt;/li>
&lt;li>&lt;strong>&lt;code>compositional-writing&lt;/code> 規則六（反例段落用正向陳述）&lt;/strong>：規則六說「反例段落要有正向概念當錨點」、本卡補規則六沒覆蓋的反向 case — &lt;strong>正向句也可能因為刪掉對照而失去依據&lt;/strong>。兩條互補：規則六防止「只有反例沒有錨點」、本卡防止「只有錨點沒有對照」。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>當你寫下或審閱以下句型時、停下來檢查：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>「X、不是 Y」這個句型在做兩件事：給結論 X、排除讀者直覺會想到的 Y。</strong> 兩者是同一個推理單元、一起出現才完整。為了滿足「正向陳述優先」而把 Y 直接刪掉、剩下的 X 就失去 contrast、變成沒依據的空降斷言。</p>
<table>
  <thead>
      <tr>
          <th>改寫方式</th>
          <th>結論是否站得住</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>保留「X、不是 Y」</td>
          <td>站得住、Y 提供 contrast、讀者知道 X 排除了什麼可能性</td>
      </tr>
      <tr>
          <td>補理由：「X、因為 …」</td>
          <td>站得住、用 reasoning 取代 contrast</td>
      </tr>
      <tr>
          <td>改對照表：A 欄列 X / B 欄列 Y</td>
          <td>站得住、把對照升級成結構化呈現</td>
      </tr>
      <tr>
          <td>直接刪 Y、只剩 X</td>
          <td>不站得住、X 變成空降斷言、讀者無法判斷依據</td>
      </tr>
      <tr>
          <td>補一個更弱的 hint（「Y 只是表象」）</td>
          <td>不站得住、hint 沒解釋為什麼是表象、等同沒寫</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="情境">情境</h2>
<p>寫作規範要求「正向陳述優先」、AI 或作者批量改寫舊文時、為了滿足 grep <code>不是 / 不要 / 避免</code> 等掃描規則、會把句子裡的否定詞機械式刪掉。但很多否定詞承擔的是 <strong>contrast 角色</strong>、不是冗餘 — 抽掉之後句子在語法上正向了、但語意上失去了推理依據。</p>
<p>具體例子（本 blog <code>compositional-writing</code> skill 的修正過程）：</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">改 1：拆分依據是**認知負擔與情境匹配度**（行數只是表象）。
</span></span><span class="line"><span class="ln">3</span><span class="cl">改 2：拆分依據是**認知負擔與情境匹配度**。</span></span></code></pre></div><ul>
<li>原文：給結論（認知負擔）+ 排除讀者直覺（行數）。Y = 行數是讀者最容易想到的替代答案、明確排除掉這條路才有意義。</li>
<li>改 1：把「不是 Y」軟化成「Y 只是表象」、但<strong>沒解釋為什麼是表象</strong>。讀者讀完不知道「行數為什麼不算依據」、結論依然空降、只是包裝成正向句。</li>
<li>改 2：直接刪、結論完全失去 contrast。</li>
</ul>
<p>兩個改法都比原文差。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>正向改寫時、先判斷被刪掉的 Y 是哪一類：</p>
<h3 id="y-是讀者直覺會想到的替代方案--保留-contrast">Y 是讀者直覺會想到的替代方案 → 保留 contrast</h3>
<p>讀者第一直覺會想到 Y、只給 X 不夠。處理方式有三：</p>
<ol>
<li><strong>保留「X、不是 Y」原句</strong>（規則六精神：反例段落要有正向錨點、但反例本身仍存在）— 這不違反「正向陳述優先」、因為「正向陳述優先」要的是「主句承載結論、反例只做對照」、不是「全段沒有否定詞」。「X、不是 Y」這個句型主句仍是 X（正向陳述）、Y 是子句裡的 contrast、結構上符合規範</li>
<li><strong>補解釋</strong>：「X、因為 [推理]」 — 用 reasoning 接管 contrast 的角色</li>
<li><strong>升級成對照表</strong>：兩欄並列 X 跟 Y、讓 contrast 變結構化呈現、不靠句法</li>
</ol>
<h3 id="y-是冗餘否定讀者不會想到-y--直接刪">Y 是冗餘否定（讀者不會想到 Y） → 直接刪</h3>
<p>例：「使用 LRU cache、不是普通 dict」— 若上下文沒有人會把 dict 當預設、Y 是冗餘、刪掉沒損失。</p>
<p>判別問題：「<strong>讀者讀到 X 之前、心裡最可能想到的答案是 Y 嗎？</strong> 是 → 保留 contrast。否 → 安全刪。」</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="結論變空降斷言讀者無法驗證">結論變空降斷言、讀者無法驗證</h3>
<p>「行數只是表象」這種句子、讀者讀完只能選兩條路：</p>
<ul>
<li>全盤接受（迷信權威）</li>
<li>自己腦補理由（每個讀者腦補不同、原則無法統一傳遞）</li>
</ul>
<p>兩條都讓「教學」失效 — 教學的目的是讓讀者拿到 reasoning、能套用到新情境、不是讓讀者背規則。</p>
<h3 id="規則崩潰壓力下會回退到刪掉的-y">規則崩潰：壓力下會回退到刪掉的 Y</h3>
<p>機會成本語氣的核心 reasoning 是「絕對主義教讀者規則（壓力下會忘）、機會成本教讀者思考方式」。空降斷言是更糟的「規則」— 連「為什麼是規則」都沒給、讀者壓力下完全無錨點、會回退到自己的直覺（也就是被刪掉的 Y）。</p>
<h3 id="批量改寫無法被-review">批量改寫無法被 review</h3>
<p>改寫成「（Y 只是表象）」「（Y 是次級訊號）」這類軟化措辭時、表面看起來完成正向化、實際上把問題藏起來 — 規範掃描器（grep <code>不是</code>）不再 catch、人類 reviewer 也容易放過、議題在文章裡持續腐爛。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></strong>：本卡是 #67 在「寫作規範執行」層的具體案例。批量正向化時、最便利的做法（grep + 機械刪否定詞）跟意圖對齊（保留推理鍊）反向 — 越容易執行、越容易產出空降斷言。</li>
<li><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精修</a></strong>：本卡是 #82 在「寫作規則」層的同形 pattern。grep <code>不是 / 不要</code> 是字面層攔截、看不到那個「不是」承擔的是 contrast 還是冗餘 — 需要 behavioral pass（讀者能否驗證結論）。</li>
<li><strong><a href="../writing-multi-pass-review/">#83 寫作的 multi-pass review</a></strong>：本卡補強 #83 的 frame 設計 — 「正向陳述」這輪掃完之後、要再加一輪「結論可驗證性」掃描、確認被改的句子仍然有推理依據。</li>
<li><strong><code>compositional-writing</code> 規則六（反例段落用正向陳述）</strong>：規則六說「反例段落要有正向概念當錨點」、本卡補規則六沒覆蓋的反向 case — <strong>正向句也可能因為刪掉對照而失去依據</strong>。兩條互補：規則六防止「只有反例沒有錨點」、本卡防止「只有錨點沒有對照」。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>當你寫下或審閱以下句型時、停下來檢查：</p>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「X、Y 只是表象 / 次級訊號 / 不重要」</td>
          <td>「Y 只是 …」沒展開原因 = 把斷言用副詞包裝、仍是空降</td>
      </tr>
      <tr>
          <td>「X」單句結論、上下文沒有對照表 / reasoning / 例子</td>
          <td>結論失去支撐、讀者無法判斷為何成立</td>
      </tr>
      <tr>
          <td>改寫前句子是「X、不是 Y」、改寫後 Y 消失</td>
          <td>檢查 Y 是否承擔 contrast、是 → 補回或升級結構化呈現</td>
      </tr>
      <tr>
          <td>Reviewer 讀完問「為什麼？」</td>
          <td>讀者沒拿到 reasoning、結論是空降</td>
      </tr>
      <tr>
          <td>自己解釋給別人聽時、會自然說「不是 Y、是 X」</td>
          <td>口語會自動補上的 contrast、文字版被機械刪掉了</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：寫作規範執行（規則化掃描 → 批量改寫）、AI 輔助寫作的 review、知識卡片 / 教學文章的論證段落</li>
<li><strong>不適用</strong>：純 reference 型內容（API 文件、規格表）— 這類內容讀者只查不推理、contrast 不重要</li>
<li><strong>邊界</strong>：「保留 contrast」≠「保留所有否定詞」— 冗餘否定（讀者不會想到的 Y）仍該刪、判別標準是「讀者直覺會不會想到 Y」</li>
</ul>
]]></content:encoded></item><item><title>Multi-pass review 的 scope 要蓋『同類風險區』、不是『改動區』</title><link>https://tarrragon.github.io/blog/report/multi-pass-scope-must-cover-risk-zone/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/multi-pass-scope-must-cover-risk-zone/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Multi-pass review 的 scope 由『同類風險區』決定、不是『我改過的檔』。&lt;/strong> 跑某條原則的 pass（例如「結論可驗證性」）、scope 就是「所有可能違反該原則的檔」、不是「我這次改了的檔」。後者是執行便利選擇、不是 review scope 的合法依據 — 同一原則違反通常分布在整個 corpus、改動區只佔小比例。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Scope 定義方式&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>整個 corpus 既有的同類違規（佔多數）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「同類風險的內容範圍」&lt;/td>
 &lt;td>該原則在整個 corpus 的所有違規&lt;/td>
 &lt;td>無 — 這就是該原則 review 的合法 scope&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「&lt;strong>這次 pass 用的 frame、適用範圍是哪些檔？我的 scope 涵蓋了那個範圍嗎？&lt;/strong>」答案是「沒有、我只掃了改動區」就需要擴大 scope。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>寫了一條新原則卡片（例：#94 結論可驗證性）後、要套回既有寫作做 multi-pass review。預設行為是「掃我這次改過的檔」、原因：&lt;/p>
&lt;ul>
&lt;li>改動區是當下 working memory 裡的內容、便利&lt;/li>
&lt;li>改動區 diff 短、容易跑完&lt;/li>
&lt;li>改動區是「我剛動過的、所以可能有違規」的直覺對象&lt;/li>
&lt;/ul>
&lt;p>這三個理由都是&lt;strong>執行便利&lt;/strong>、不是「review scope 的合法依據」。新原則套回既有 corpus 時、違規通常已經存在於沒被改動的部分（因為原則之前不存在、整個 corpus 都沒按該原則寫過）。只掃改動區會 systematic miss 大部分違規。&lt;/p>
&lt;p>具體 case：本 blog &lt;code>compositional-writing&lt;/code> skill 的負向表述清理：&lt;/p>
&lt;ol>
&lt;li>寫 #94 結論可驗證性原則（contrast 該保留還是該刪的判別）&lt;/li>
&lt;li>Multi-pass 跑 6 輪、scope 設為「我改過的 SKILL.md / writing-articles.md / #94 卡片本身」&lt;/li>
&lt;li>第二天使用者讀 references/writing-prompts.md、發現「Prompt 的『原子單位』不是『一句話』、而是『一個可被驗收的任務』」就是 #94 該抓的 case&lt;/li>
&lt;li>該違規分布廣度：scan 整個 references/ 目錄、相同句型違規共 4 處（writing-prompts / writing-logs / designing-fields / managing-article-collections）、僅 0 處在我改動區內&lt;/li>
&lt;/ol>
&lt;p>我的 multi-pass 設計把 4 處違規 100% 漏掉、使用者用肉眼讀就抓到。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步定義原則的適用範圍">第一步：定義原則的「適用範圍」&lt;/h3>
&lt;p>寫一條新原則時、明確標註該原則適用於哪些檔 / section。例：&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>#94 結論可驗證性&lt;/td>
 &lt;td>所有教學 / 知識卡 / 規範文件的論證段落&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規則五 最重要的話優先說&lt;/td>
 &lt;td>所有有「核心命題」「重點」段落的文件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Severity 判準對齊分流目的&lt;/td>
 &lt;td>所有定義 severity 的 logging / monitoring 文件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="第二步pass-scope--適用範圍--待-review-corpus">第二步：Pass scope = 適用範圍 ∩ 待 review corpus&lt;/h3>
&lt;p>跑該原則的 pass 時、scope 是「該原則的適用範圍」交集「目前要 review 的 corpus」、跟「我改過哪些檔」完全無關。&lt;/p>
&lt;p>對 compositional-writing skill 而言：跑 #94 pass 的合法 scope 是 &lt;code>.claude/skills/compositional-writing/**/*.md&lt;/code> 全部、不是「我這次 commit 動到的 5 個檔」。&lt;/p>
&lt;h3 id="第三步用-grep-把同類風險區找出來">第三步：用 grep 把同類風險區找出來&lt;/h3>
&lt;p>同類風險常有結構性 signature（句型、詞彙、模式）、可用 grep 把適用範圍縮到具體行。例：&lt;/p>
&lt;ul>
&lt;li>#94：grep &lt;code>不是.*而是\|不是.*是\|只是表象\|只是次級訊號&lt;/code> → 候選行&lt;/li>
&lt;li>規則五：grep &lt;code>^[^*].*。.*它&lt;/code> → 「X。它不是…」後置定義候選&lt;/li>
&lt;li>Severity 判準：grep &lt;code>severity.*\(嚴重度\|頻率\)&lt;/code> → 替代判準誤用候選&lt;/li>
&lt;/ul>
&lt;p>把 grep 命中行當 review 起點、再人工判斷是否真違規。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="multi-pass-退化成-self-edit-pass">Multi-pass 退化成 self-edit pass&lt;/h3>
&lt;p>只掃改動區 = 把 multi-pass review 退化成「我剛寫的東西自己再看一次」、這是 &lt;a href="../writing-multi-pass-review/">#83&lt;/a> 「同 frame 重看」的另一種變形 — 表面在跑 N 輪、實際 scope 是同一塊小區。違規率沒下降、只是把改動區內的違規多檢查了 N 次。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Multi-pass review 的 scope 由『同類風險區』決定、不是『我改過的檔』。</strong> 跑某條原則的 pass（例如「結論可驗證性」）、scope 就是「所有可能違反該原則的檔」、不是「我這次改了的檔」。後者是執行便利選擇、不是 review scope 的合法依據 — 同一原則違反通常分布在整個 corpus、改動區只佔小比例。</p>
<table>
  <thead>
      <tr>
          <th>Scope 定義方式</th>
          <th>抓得到的違規</th>
          <th>漏掉的違規</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「我改過的檔」（便利）</td>
          <td>改動區的新引入違規 + 我順手注意到的舊違規</td>
          <td>整個 corpus 既有的同類違規（佔多數）</td>
      </tr>
      <tr>
          <td>「同類風險的內容範圍」</td>
          <td>該原則在整個 corpus 的所有違規</td>
          <td>無 — 這就是該原則 review 的合法 scope</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「<strong>這次 pass 用的 frame、適用範圍是哪些檔？我的 scope 涵蓋了那個範圍嗎？</strong>」答案是「沒有、我只掃了改動區」就需要擴大 scope。</p>
<hr>
<h2 id="情境">情境</h2>
<p>寫了一條新原則卡片（例：#94 結論可驗證性）後、要套回既有寫作做 multi-pass review。預設行為是「掃我這次改過的檔」、原因：</p>
<ul>
<li>改動區是當下 working memory 裡的內容、便利</li>
<li>改動區 diff 短、容易跑完</li>
<li>改動區是「我剛動過的、所以可能有違規」的直覺對象</li>
</ul>
<p>這三個理由都是<strong>執行便利</strong>、不是「review scope 的合法依據」。新原則套回既有 corpus 時、違規通常已經存在於沒被改動的部分（因為原則之前不存在、整個 corpus 都沒按該原則寫過）。只掃改動區會 systematic miss 大部分違規。</p>
<p>具體 case：本 blog <code>compositional-writing</code> skill 的負向表述清理：</p>
<ol>
<li>寫 #94 結論可驗證性原則（contrast 該保留還是該刪的判別）</li>
<li>Multi-pass 跑 6 輪、scope 設為「我改過的 SKILL.md / writing-articles.md / #94 卡片本身」</li>
<li>第二天使用者讀 references/writing-prompts.md、發現「Prompt 的『原子單位』不是『一句話』、而是『一個可被驗收的任務』」就是 #94 該抓的 case</li>
<li>該違規分布廣度：scan 整個 references/ 目錄、相同句型違規共 4 處（writing-prompts / writing-logs / designing-fields / managing-article-collections）、僅 0 處在我改動區內</li>
</ol>
<p>我的 multi-pass 設計把 4 處違規 100% 漏掉、使用者用肉眼讀就抓到。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步定義原則的適用範圍">第一步：定義原則的「適用範圍」</h3>
<p>寫一條新原則時、明確標註該原則適用於哪些檔 / section。例：</p>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>適用範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>#94 結論可驗證性</td>
          <td>所有教學 / 知識卡 / 規範文件的論證段落</td>
      </tr>
      <tr>
          <td>規則五 最重要的話優先說</td>
          <td>所有有「核心命題」「重點」段落的文件</td>
      </tr>
      <tr>
          <td>Severity 判準對齊分流目的</td>
          <td>所有定義 severity 的 logging / monitoring 文件</td>
      </tr>
  </tbody>
</table>
<h3 id="第二步pass-scope--適用範圍--待-review-corpus">第二步：Pass scope = 適用範圍 ∩ 待 review corpus</h3>
<p>跑該原則的 pass 時、scope 是「該原則的適用範圍」交集「目前要 review 的 corpus」、跟「我改過哪些檔」完全無關。</p>
<p>對 compositional-writing skill 而言：跑 #94 pass 的合法 scope 是 <code>.claude/skills/compositional-writing/**/*.md</code> 全部、不是「我這次 commit 動到的 5 個檔」。</p>
<h3 id="第三步用-grep-把同類風險區找出來">第三步：用 grep 把同類風險區找出來</h3>
<p>同類風險常有結構性 signature（句型、詞彙、模式）、可用 grep 把適用範圍縮到具體行。例：</p>
<ul>
<li>#94：grep <code>不是.*而是\|不是.*是\|只是表象\|只是次級訊號</code> → 候選行</li>
<li>規則五：grep <code>^[^*].*。.*它</code> → 「X。它不是…」後置定義候選</li>
<li>Severity 判準：grep <code>severity.*\(嚴重度\|頻率\)</code> → 替代判準誤用候選</li>
</ul>
<p>把 grep 命中行當 review 起點、再人工判斷是否真違規。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="multi-pass-退化成-self-edit-pass">Multi-pass 退化成 self-edit pass</h3>
<p>只掃改動區 = 把 multi-pass review 退化成「我剛寫的東西自己再看一次」、這是 <a href="../writing-multi-pass-review/">#83</a> 「同 frame 重看」的另一種變形 — 表面在跑 N 輪、實際 scope 是同一塊小區。違規率沒下降、只是把改動區內的違規多檢查了 N 次。</p>
<h3 id="既有-corpus-的違規永久累積">既有 corpus 的違規永久累積</h3>
<p>每次跑 pass 都只掃改動區、整個 corpus 的既有違規會永久不被處理 — 因為新加的原則只回頭掃改動區、舊內容沒被改的就永遠不過 review。corpus 越大、這個問題越嚴重 — 違規被結構性掩蓋、不是因為沒人看、是因為 scope 預設把它們排除。</p>
<h3 id="違規由使用者眼睛-catch而不是流程-catch">違規由使用者眼睛 catch、而不是流程 catch</h3>
<p>最終結果是違規被使用者讀到才發現、而不是 review 流程主動 catch。這違反 <a href="../literal-interception-vs-behavioral-refinement/">#82</a> 跟 <a href="../external-trigger-for-high-roi-work/">#72</a> — 把高 ROI 的 review 工作結構性跳過、靠外部 trigger（使用者抱怨）才執行。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></strong>：本卡是 #67 在「review scope 設定」層的具體案例。最便利的 scope（改動區）跟意圖對齊（同類風險區）反向 — 越便利、越漏掉違規。</li>
<li><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></strong>：本卡跟 #82 互補。#82 是「驗證粒度」要對齊、本卡是「驗證範圍」要對齊 — 兩個都是把 review 結構錯位、結果都是 false confidence。</li>
<li><strong><a href="../writing-multi-pass-review/">#83 寫作的 multi-pass review</a></strong>：本卡補強 #83 沒覆蓋的 scope 軸。#83 講「每輪換 frame」（horizontal）、本卡講「每輪 scope 由原則適用範圍決定」（範圍）— frame × scope 兩軸都對齊、multi-pass 才有意義。</li>
<li><strong><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發的工作會被結構性跳過</a></strong>：「擴大 scope 跑 corpus-wide review」是高 ROI 但無強制 trigger 的工作、本卡指出該動作預設會被「改動區便利 scope」結構性取代。</li>
<li><strong><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據</a></strong>：本卡跟 #94 是同次事故串接 — #94 抓到「改寫違規」、本卡抓到「review scope 漏掉的違規」、兩條一起才是完整的「正向改寫流程」。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>當你準備 / 正在跑 multi-pass review 時、停下來檢查：</p>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pass 的 scope 是「我改過的檔」</td>
          <td>便利 scope、跟原則適用範圍對不上</td>
      </tr>
      <tr>
          <td>沒明確標註「這條原則適用於哪些檔」</td>
          <td>沒適用範圍 = scope 沒依據、預設就會回退到改動區</td>
      </tr>
      <tr>
          <td>跑完 pass 沒用 grep 把同類風險區掃過一遍</td>
          <td>缺結構性掃描、漏掉的違規無法被流程 catch</td>
      </tr>
      <tr>
          <td>Review 報告寫「修了 X 處違規」、沒寫「掃過 Y 個檔」</td>
          <td>報告只記輸出、沒記 scope、無法事後驗證 review 範圍是否合法</td>
      </tr>
      <tr>
          <td>使用者後續抓到違規、且該違規不在你的改動區</td>
          <td>scope 失誤的 retro signal、表示這條原則的 pass 該 corpus-wide</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：multi-pass review、新原則套回既有 corpus、批量品質檢查</li>
<li><strong>不適用</strong>：純 syntax / lint check（這類由工具固定 scope 跑全 corpus、無便利退化問題）</li>
<li><strong>邊界</strong>：「同類風險區」≠「全 repo 全掃」— scope 由原則適用範圍界定、不無限擴大；範圍邊界比較適合寫在原則定義時（一次想清楚、之後直接套用）、勝過 review 時臨時決定（每次都要重判一次、累積成本高）</li>
</ul>
]]></content:encoded></item><item><title>適用範圍要展開成 file enumeration、口語描述不夠</title><link>https://tarrragon.github.io/blog/report/applicability-scope-must-be-enumerated/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/applicability-scope-must-be-enumerated/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>適用範圍要展開成具體 file enumeration、不能只給口語類型描述。&lt;/strong> 「所有教學文件」「所有規範文件」這類描述聽起來夠語意化、實際執行 review 時要當場推導「具體哪些檔屬於這個類型」— 推導步驟容易漏。Enumerate 成具體 file list（或可重現該 list 的 grep / find 指令）才是合法的適用範圍形式。&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>不夠、執行時要當場推導具體檔、推導步驟易漏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「&lt;code>.claude/skills/compositional-writing/**/*.md&lt;/code>」&lt;/td>
 &lt;td>合法、明確檔列表、可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「&lt;code>grep -l '## 核心原則' content/report/&lt;/code>」&lt;/td>
 &lt;td>合法、可重現的 grep 規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「同類教學文件、含 mirror / fork / 翻譯版」&lt;/td>
 &lt;td>不夠、mirror / fork / 翻譯版要明列具體 path&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「&lt;strong>這個適用範圍能 reproduce 出具體 file list 嗎？兩個人各自展開會得到同一個 list 嗎？&lt;/strong>」答案是「不能」就需要 enumerate。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>寫了一條原則卡片（例：#95 multi-pass scope 由適用範圍決定）後、開始套用該原則跑 review。預設行為是讀「適用範圍」描述、心裡推導具體檔、開始掃。推導過程典型遇到的事故：&lt;/p>
&lt;ul>
&lt;li>描述用語意層詞彙（「教學文件」「規範文件」）、心裡只想到主檔、忘了 mirror / sibling / 翻譯版&lt;/li>
&lt;li>描述用 directory 層級（「&lt;code>.claude/skills/&lt;/code>」）、忘了 &lt;code>content/skills/&lt;/code> 是同 surface mirror&lt;/li>
&lt;li>描述用「所有 X」，X 邊界本來就模糊（「所有 Pattern 卡片」— 哪些算 Pattern？）&lt;/li>
&lt;/ul>
&lt;p>具體 case：本 blog 跑 #94 + #95 review 時、適用範圍寫「&lt;code>compositional-writing&lt;/code> skill 的所有 references」、心裡推導 = &lt;code>.claude/skills/compositional-writing/references/*.md&lt;/code>。漏掉的是：&lt;/p>
&lt;ul>
&lt;li>&lt;code>content/skills/compositional-writing/*.md&lt;/code>（同 surface mirror、AGENTS.md Skill 撰寫規範要求「主體相同」）&lt;/li>
&lt;li>同名檔在兩個物理路徑都存在、語意上是同一份內容&lt;/li>
&lt;/ul>
&lt;p>連續兩輪 review（#94 跟 #95）都漏同 5 個 mirror 檔、共 10 處違規透過 mirror 永久躲過 review。&lt;strong>Root cause 不是「我忘了」、是「適用範圍沒 enumerate、每次 review 都要重新心算一次具體 list」&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步原則定義時-enumerate-適用範圍">第一步：原則定義時 enumerate 適用範圍&lt;/h3>
&lt;p>寫原則卡片時、適用範圍欄位寫具體 file enumeration（path glob 或 grep / find 指令）、不寫類型描述。例：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gu">## 適用範圍
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> &lt;span class="sb">`.claude/skills/compositional-writing/SKILL.md`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> &lt;span class="sb">`.claude/skills/compositional-writing/references/*.md`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="k">-&lt;/span> &lt;span class="sb">`content/skills/compositional-writing/*.md`&lt;/span>（mirror、AGENTS.md §9 規範同步）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">- &lt;span class="sb">`content/report/*.md`&lt;/span>（教學卡）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>或用可重現的 grep / find 規則：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 適用範圍 = 含「## 核心原則」section 的所有教學卡&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">grep -rl &lt;span class="s2">&amp;#34;^## 核心原則&amp;#34;&lt;/span> content/report/ content/skills/&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩種形式擇一、避免「所有 X 類文件」的口語描述。&lt;/p>
&lt;h3 id="第二步review-開始前先跑-enumeration確認-list-完整">第二步：Review 開始前先跑 enumeration、確認 list 完整&lt;/h3>
&lt;p>跑 pass 之前、先 ls / grep 出具體檔列表、貼到 review 紀錄裡、確認沒漏。例：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">$ find .claude/skills/compositional-writing content/skills/compositional-writing -name &lt;span class="s2">&amp;#34;*.md&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">.claude/skills/compositional-writing/SKILL.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">.claude/skills/compositional-writing/references/writing-articles.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">... &lt;span class="o">(&lt;/span>&lt;span class="m">15&lt;/span> files total&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">content/skills/compositional-writing/skill.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">content/skills/compositional-writing/writing-articles.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">... &lt;span class="o">(&lt;/span>&lt;span class="m">12&lt;/span> files total&lt;span class="o">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把這個 list 當這次 pass 的 ground truth、跟原則的「適用範圍 enumeration」比對、有 discrepancy（list 多 / 少）就停下來重看適用範圍定義。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>適用範圍要展開成具體 file enumeration、不能只給口語類型描述。</strong> 「所有教學文件」「所有規範文件」這類描述聽起來夠語意化、實際執行 review 時要當場推導「具體哪些檔屬於這個類型」— 推導步驟容易漏。Enumerate 成具體 file list（或可重現該 list 的 grep / find 指令）才是合法的適用範圍形式。</p>
<table>
  <thead>
      <tr>
          <th>適用範圍的形式</th>
          <th>合法性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「所有教學 / 知識卡 / 規範文件的論證段落」</td>
          <td>不夠、執行時要當場推導具體檔、推導步驟易漏</td>
      </tr>
      <tr>
          <td>「<code>.claude/skills/compositional-writing/**/*.md</code>」</td>
          <td>合法、明確檔列表、可重現</td>
      </tr>
      <tr>
          <td>「<code>grep -l '## 核心原則' content/report/</code>」</td>
          <td>合法、可重現的 grep 規則</td>
      </tr>
      <tr>
          <td>「同類教學文件、含 mirror / fork / 翻譯版」</td>
          <td>不夠、mirror / fork / 翻譯版要明列具體 path</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「<strong>這個適用範圍能 reproduce 出具體 file list 嗎？兩個人各自展開會得到同一個 list 嗎？</strong>」答案是「不能」就需要 enumerate。</p>
<hr>
<h2 id="情境">情境</h2>
<p>寫了一條原則卡片（例：#95 multi-pass scope 由適用範圍決定）後、開始套用該原則跑 review。預設行為是讀「適用範圍」描述、心裡推導具體檔、開始掃。推導過程典型遇到的事故：</p>
<ul>
<li>描述用語意層詞彙（「教學文件」「規範文件」）、心裡只想到主檔、忘了 mirror / sibling / 翻譯版</li>
<li>描述用 directory 層級（「<code>.claude/skills/</code>」）、忘了 <code>content/skills/</code> 是同 surface mirror</li>
<li>描述用「所有 X」，X 邊界本來就模糊（「所有 Pattern 卡片」— 哪些算 Pattern？）</li>
</ul>
<p>具體 case：本 blog 跑 #94 + #95 review 時、適用範圍寫「<code>compositional-writing</code> skill 的所有 references」、心裡推導 = <code>.claude/skills/compositional-writing/references/*.md</code>。漏掉的是：</p>
<ul>
<li><code>content/skills/compositional-writing/*.md</code>（同 surface mirror、AGENTS.md Skill 撰寫規範要求「主體相同」）</li>
<li>同名檔在兩個物理路徑都存在、語意上是同一份內容</li>
</ul>
<p>連續兩輪 review（#94 跟 #95）都漏同 5 個 mirror 檔、共 10 處違規透過 mirror 永久躲過 review。<strong>Root cause 不是「我忘了」、是「適用範圍沒 enumerate、每次 review 都要重新心算一次具體 list」</strong>。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步原則定義時-enumerate-適用範圍">第一步：原則定義時 enumerate 適用範圍</h3>
<p>寫原則卡片時、適用範圍欄位寫具體 file enumeration（path glob 或 grep / find 指令）、不寫類型描述。例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 適用範圍
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">-</span> <span class="sb">`.claude/skills/compositional-writing/SKILL.md`</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">-</span> <span class="sb">`.claude/skills/compositional-writing/references/*.md`</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">-</span> <span class="sb">`content/skills/compositional-writing/*.md`</span>（mirror、AGENTS.md §9 規範同步）
</span></span><span class="line"><span class="ln">6</span><span class="cl">- <span class="sb">`content/report/*.md`</span>（教學卡）</span></span></code></pre></div><p>或用可重現的 grep / find 規則：</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"># 適用範圍 = 含「## 核心原則」section 的所有教學卡</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep -rl <span class="s2">&#34;^## 核心原則&#34;</span> content/report/ content/skills/</span></span></code></pre></div><p>兩種形式擇一、避免「所有 X 類文件」的口語描述。</p>
<h3 id="第二步review-開始前先跑-enumeration確認-list-完整">第二步：Review 開始前先跑 enumeration、確認 list 完整</h3>
<p>跑 pass 之前、先 ls / grep 出具體檔列表、貼到 review 紀錄裡、確認沒漏。例：</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">$ find .claude/skills/compositional-writing content/skills/compositional-writing -name <span class="s2">&#34;*.md&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">.claude/skills/compositional-writing/SKILL.md
</span></span><span class="line"><span class="ln">3</span><span class="cl">.claude/skills/compositional-writing/references/writing-articles.md
</span></span><span class="line"><span class="ln">4</span><span class="cl">... <span class="o">(</span><span class="m">15</span> files total<span class="o">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">content/skills/compositional-writing/skill.md
</span></span><span class="line"><span class="ln">6</span><span class="cl">content/skills/compositional-writing/writing-articles.md
</span></span><span class="line"><span class="ln">7</span><span class="cl">... <span class="o">(</span><span class="m">12</span> files total<span class="o">)</span></span></span></code></pre></div><p>把這個 list 當這次 pass 的 ground truth、跟原則的「適用範圍 enumeration」比對、有 discrepancy（list 多 / 少）就停下來重看適用範圍定義。</p>
<h3 id="第三步把-enumeration-規則做成工具化檢查">第三步：把 enumeration 規則做成工具化檢查</h3>
<p>當 enumeration 規則穩定後（例：「<code>.claude/skills/&lt;x&gt;/</code> 跟 <code>content/skills/&lt;x&gt;/</code> 永遠是 mirror 關係」）、把它寫成 lint / pre-commit hook：</p>
<ul>
<li>mirror 檢查：兩 path 同名檔有 diff → 警告</li>
<li>enumeration 漂移檢查：原則卡片裡的 path glob 跟實際 ls 結果有差 → 警告</li>
</ul>
<p>工具化後 enumeration 不再依賴人為紀律、是結構性保證。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="review-scope-每次都要重新心算漏判率隨輪次累積">Review scope 每次都要重新心算、漏判率隨輪次累積</h3>
<p>口語描述的適用範圍 = 每次 review 都要當場推導具體 list。推導步驟有錯漏率、每跑一輪 review 就累積一次。本 blog 跑 #94 / #95 連續兩輪都漏同 5 個 mirror = 推導錯漏率 100%、不是偶發。</p>
<h3 id="mirror--fork--副本永遠躲過-review">Mirror / fork / 副本永遠躲過 review</h3>
<p>語意上是同一份內容、物理上不同 path 的檔（mirror / fork / 翻譯版 / SDK 多語言 port），最容易在「所有 X」描述下被漏掉 — 因為心算推導對齊到「主檔」、副本被「以為已包含」漏跳過。連續多輪後副本內容會嚴重 drift、跟主檔失去同步。</p>
<h3 id="enumeration-缺失等於沒有原則的-ssot">Enumeration 缺失等於沒有原則的 SSoT</h3>
<p>適用範圍是原則的「作用域 SSoT」、沒寫清楚就等於每個讀者各自解釋一份 — #44 SSoT 違反在「原則作用域」維度的具體形態。讀者 A 跑 review 涵蓋 mirror、讀者 B 不涵蓋、原則套用結果就會 drift。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass scope 要蓋同類風險區</a></strong>：本卡是 #95 的下游具體化。#95 答「scope 從哪來 = 適用範圍 ∩ corpus」、本卡答「適用範圍長什麼樣 = enumerated file list」。兩條串起來才是完整 review 流程。</li>
<li><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></strong>：本卡跟 #82 互補。Enumerate file list 是字面層（具體 path）、enumeration completeness 是行為層的合法性判準（兩個人展開能否得到同一個 list）— 兩層都要對齊、scope 才合法。</li>
<li><strong><a href="../single-source-of-truth/">#44 Single Source of Truth</a></strong>：本卡是 #44 在「原則作用域」維度的具體案例。適用範圍口語描述 = 每個讀者各自解釋一次、結果 drift。</li>
<li><strong><a href="../measurement-completeness/">#7 量測值缺一不可</a></strong>：本卡是 #7 在「review 範圍」的同形 pattern — enumerate 漏一個 = sanity 防線有缺口、整組 review 結果不可信。</li>
<li><strong><a href="../two-occurrence-threshold/">#42 2 次門檻是訊號</a></strong>：本卡的觸發來自「同方向漏判 2 次」訊號 — mirror 漏同步在 #94 / #95 review 連續發生、就是 #42 訊號要求抽象的 case。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>當你寫原則卡片或準備跑 review 時、停下來檢查：</p>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>適用範圍寫「所有 X 類文件」（口語類型描述）</td>
          <td>沒 enumerate、執行時要心算、易漏副本</td>
      </tr>
      <tr>
          <td>適用範圍只寫主目錄、沒列 mirror / fork / 翻譯版</td>
          <td>mirror 系列檔最容易漏、明列</td>
      </tr>
      <tr>
          <td>兩個人讀同一個適用範圍、心裡展開的 file list 不一樣</td>
          <td>適用範圍模糊、要改寫成 grep / find 規則</td>
      </tr>
      <tr>
          <td>Review 跑完、沒有「實際掃過哪些檔」的紀錄</td>
          <td>scope 沒驗證、漏判無 retro signal</td>
      </tr>
      <tr>
          <td>連續多輪 review 都漏同類檔（例 mirror）</td>
          <td>#42 訊號、enumerate 規則該升級成工具化檢查</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用範圍 enumeration</strong>：
<ul>
<li><code>content/report/*.md</code>（所有 report 卡片寫適用範圍時）</li>
<li><code>.claude/skills/*/SKILL.md</code>（所有 skill 入口的觸發路由）</li>
<li><code>.claude/rules/**/*.md</code>（所有規範文件的作用域）</li>
</ul>
</li>
<li><strong>不適用</strong>：純文字創作（散文 / 詩 / 故事）— 這類沒有「原則作用域」議題、enumerate 是 over-engineering</li>
<li><strong>邊界</strong>：「Enumerate」≠「全 repo 全列」— 適用範圍仍由原則決定、enumerate 只是把該範圍寫成可重現形式；範圍本身的合理性是另一件事（由原則的 trade-off 決定）</li>
</ul>
]]></content:encoded></item><item><title>Metadata surface 要納入寫作 review 範圍</title><link>https://tarrragon.github.io/blog/report/metadata-surface-in-writing-review/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/metadata-surface-in-writing-review/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>寫作 review 的 surface 包含正文與 metadata surface。&lt;/strong> Title、description、frontmatter、heading、link label、MOC 索引條都是讀者入口與 grep 入口；它們和正文共同建立讀者第一個概念錨點。正文通過 multi-pass review 只代表 body surface 收斂，metadata surface 仍要跑同一套意圖、語氣、grep-ability 與索引一致性檢查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Surface&lt;/th>
 &lt;th>典型位置&lt;/th>
 &lt;th>Review 責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Body surface&lt;/td>
 &lt;td>段落、表格、範例、判讀徵兆&lt;/td>
 &lt;td>完整論證、段首核心、案例補足&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metadata surface&lt;/td>
 &lt;td>&lt;code>title&lt;/code>、&lt;code>description&lt;/code>、&lt;code>tags&lt;/code>、&lt;code>weight&lt;/code>&lt;/td>
 &lt;td>讀者第一眼、搜尋摘要、排序與分類&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Navigation surface&lt;/td>
 &lt;td>&lt;code>_index.md&lt;/code> 索引條、MOC hook、link label&lt;/td>
 &lt;td>跨篇路由、下一步判斷、概念入口一致性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Identity surface&lt;/td>
 &lt;td>檔名、slug、canonical link&lt;/td>
 &lt;td>可回溯識別、跨工具定位、單次 grep 命中&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「&lt;strong>讀者看到這篇文章之前，會先看到哪些文字？這些文字有沒有跟正文跑同一輪 review？&lt;/strong>」&lt;/p>
&lt;hr>
&lt;h2 id="warp-分析摘要">WARP 分析摘要&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>觀察&lt;/td>
 &lt;td>在建立資安章節大綱時，正文已採用「資安作為風險路由系統」的正向概念，但 frontmatter title 與 &lt;code>_index.md&lt;/code> 索引條保留 &lt;code>資安不是 Checklist：它是風險路由系統&lt;/code>。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>判讀&lt;/td>
 &lt;td>Review frame 套在 body surface，metadata surface 被當成包裝文字；因此「正向陳述優先」實際只覆蓋正文，讀者入口仍使用負向 hook。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>策略&lt;/td>
 &lt;td>把 metadata surface 明列成 review scope：title、description、tags、heading、link label、MOC hook、slug / filename 都要跟正文一起跑 positive wording、focus、grep-ability、cross-link pass。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結論&lt;/td>
 &lt;td>&lt;code>compositional-writing&lt;/code> 的 multi-pass 規則需要補一個 surface 軸：frame 決定看什麼品質，surface 決定掃哪些文字。Frame × surface 同時完整，review 才能覆蓋文章實際被讀到的位置。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向驗證：有些標題可以保留對照句型，條件是正文需要讀者先排除常見誤解，且標題本身同時給出正向概念錨點。這次的正文已能用「資安作為風險路由系統」直接建立錨點，對照句型放在正文的論證段更穩定。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>建立 &lt;code>content/backend/07-security-data-protection/security-as-risk-routing-system.md&lt;/code> 時，文章責任已經寫成「把資安從檢查項目轉成工程路由語言」。正文段落也使用了正向定義：資安路由系統先判斷風險落點，再選擇控制面。&lt;/p>
&lt;p>問題出現在讀者入口：&lt;/p>
&lt;ul>
&lt;li>Frontmatter &lt;code>title&lt;/code> 使用 &lt;code>資安不是 Checklist：它是風險路由系統&lt;/code>&lt;/li>
&lt;li>&lt;code>content/backend/07-security-data-protection/_index.md&lt;/code> 的索引條沿用同一個 link label&lt;/li>
&lt;li>Review 討論集中在正文與章節內容，title / MOC hook 沒被列為同一輪檢查對象&lt;/li>
&lt;/ul>
&lt;p>這個問題的主因是 review surface enumeration 漏列：執行者知道要跑正向陳述檢查，但心中 scope 等於「正文段落」，沒有把 metadata surface 視為同等重要的文字。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步先列出本次產出的所有-surface">第一步：先列出本次產出的所有 surface&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">content/backend/07-security-data-protection/security-as-risk-routing-system.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">- frontmatter.title
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">- frontmatter.description
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">- body headings
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">- body paragraphs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">- link labels
&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">content/backend/07-security-data-protection/_index.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">- table link label
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">- table topic
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">- table responsibility&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這份清單是 review 的 surface enumeration。它補足 &lt;a href="../applicability-scope-must-be-enumerated/">#96 適用範圍要展開成 file enumeration&lt;/a> 的單檔內版本：#96 先列「哪些檔」，本卡再列「檔內哪些文字位置」。&lt;/p>
&lt;h3 id="第二步每一輪-frame-都掃所有-surface">第二步：每一輪 frame 都掃所有 surface&lt;/h3>
&lt;p>Multi-pass review 的每輪 frame 都要套到 surface 清單上：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Frame&lt;/th>
 &lt;th>Body surface&lt;/th>
 &lt;th>Metadata / navigation surface&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>對意圖&lt;/td>
 &lt;td>段落是否回到核心責任&lt;/td>
 &lt;td>Title / description 是否承接同一個核心責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>正向陳述 / 機會成本語氣&lt;/td>
 &lt;td>段落是否先建立概念，再補對照&lt;/td>
 &lt;td>Title / MOC hook 是否先給正向錨點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Grep-ability / 命名&lt;/td>
 &lt;td>段首關鍵字是否可搜尋&lt;/td>
 &lt;td>Title、slug、link label 是否能單次 grep 命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-link 健康度&lt;/td>
 &lt;td>引用是否指向正確卡片&lt;/td>
 &lt;td>&lt;code>_index.md&lt;/code> 索引條是否導向同一個概念入口&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>Surface enumeration 讓「我有跑正向陳述 pass」變成可驗證動作，而不只是抽象自我宣告。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>寫作 review 的 surface 包含正文與 metadata surface。</strong> Title、description、frontmatter、heading、link label、MOC 索引條都是讀者入口與 grep 入口；它們和正文共同建立讀者第一個概念錨點。正文通過 multi-pass review 只代表 body surface 收斂，metadata surface 仍要跑同一套意圖、語氣、grep-ability 與索引一致性檢查。</p>
<table>
  <thead>
      <tr>
          <th>Surface</th>
          <th>典型位置</th>
          <th>Review 責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Body surface</td>
          <td>段落、表格、範例、判讀徵兆</td>
          <td>完整論證、段首核心、案例補足</td>
      </tr>
      <tr>
          <td>Metadata surface</td>
          <td><code>title</code>、<code>description</code>、<code>tags</code>、<code>weight</code></td>
          <td>讀者第一眼、搜尋摘要、排序與分類</td>
      </tr>
      <tr>
          <td>Navigation surface</td>
          <td><code>_index.md</code> 索引條、MOC hook、link label</td>
          <td>跨篇路由、下一步判斷、概念入口一致性</td>
      </tr>
      <tr>
          <td>Identity surface</td>
          <td>檔名、slug、canonical link</td>
          <td>可回溯識別、跨工具定位、單次 grep 命中</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「<strong>讀者看到這篇文章之前，會先看到哪些文字？這些文字有沒有跟正文跑同一輪 review？</strong>」</p>
<hr>
<h2 id="warp-分析摘要">WARP 分析摘要</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觀察</td>
          <td>在建立資安章節大綱時，正文已採用「資安作為風險路由系統」的正向概念，但 frontmatter title 與 <code>_index.md</code> 索引條保留 <code>資安不是 Checklist：它是風險路由系統</code>。</td>
      </tr>
      <tr>
          <td>判讀</td>
          <td>Review frame 套在 body surface，metadata surface 被當成包裝文字；因此「正向陳述優先」實際只覆蓋正文，讀者入口仍使用負向 hook。</td>
      </tr>
      <tr>
          <td>策略</td>
          <td>把 metadata surface 明列成 review scope：title、description、tags、heading、link label、MOC hook、slug / filename 都要跟正文一起跑 positive wording、focus、grep-ability、cross-link pass。</td>
      </tr>
      <tr>
          <td>結論</td>
          <td><code>compositional-writing</code> 的 multi-pass 規則需要補一個 surface 軸：frame 決定看什麼品質，surface 決定掃哪些文字。Frame × surface 同時完整，review 才能覆蓋文章實際被讀到的位置。</td>
      </tr>
  </tbody>
</table>
<p>反向驗證：有些標題可以保留對照句型，條件是正文需要讀者先排除常見誤解，且標題本身同時給出正向概念錨點。這次的正文已能用「資安作為風險路由系統」直接建立錨點，對照句型放在正文的論證段更穩定。</p>
<hr>
<h2 id="情境">情境</h2>
<p>建立 <code>content/backend/07-security-data-protection/security-as-risk-routing-system.md</code> 時，文章責任已經寫成「把資安從檢查項目轉成工程路由語言」。正文段落也使用了正向定義：資安路由系統先判斷風險落點，再選擇控制面。</p>
<p>問題出現在讀者入口：</p>
<ul>
<li>Frontmatter <code>title</code> 使用 <code>資安不是 Checklist：它是風險路由系統</code></li>
<li><code>content/backend/07-security-data-protection/_index.md</code> 的索引條沿用同一個 link label</li>
<li>Review 討論集中在正文與章節內容，title / MOC hook 沒被列為同一輪檢查對象</li>
</ul>
<p>這個問題的主因是 review surface enumeration 漏列：執行者知道要跑正向陳述檢查，但心中 scope 等於「正文段落」，沒有把 metadata surface 視為同等重要的文字。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步先列出本次產出的所有-surface">第一步：先列出本次產出的所有 surface</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">content/backend/07-security-data-protection/security-as-risk-routing-system.md
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">- frontmatter.title
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">- frontmatter.description
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">- body headings
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">- body paragraphs
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">- link labels
</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">content/backend/07-security-data-protection/_index.md
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">- table link label
</span></span><span class="line"><span class="ln">10</span><span class="cl">- table topic
</span></span><span class="line"><span class="ln">11</span><span class="cl">- table responsibility</span></span></code></pre></div><p>這份清單是 review 的 surface enumeration。它補足 <a href="../applicability-scope-must-be-enumerated/">#96 適用範圍要展開成 file enumeration</a> 的單檔內版本：#96 先列「哪些檔」，本卡再列「檔內哪些文字位置」。</p>
<h3 id="第二步每一輪-frame-都掃所有-surface">第二步：每一輪 frame 都掃所有 surface</h3>
<p>Multi-pass review 的每輪 frame 都要套到 surface 清單上：</p>
<table>
  <thead>
      <tr>
          <th>Frame</th>
          <th>Body surface</th>
          <th>Metadata / navigation surface</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對意圖</td>
          <td>段落是否回到核心責任</td>
          <td>Title / description 是否承接同一個核心責任</td>
      </tr>
      <tr>
          <td>正向陳述 / 機會成本語氣</td>
          <td>段落是否先建立概念，再補對照</td>
          <td>Title / MOC hook 是否先給正向錨點</td>
      </tr>
      <tr>
          <td>Grep-ability / 命名</td>
          <td>段首關鍵字是否可搜尋</td>
          <td>Title、slug、link label 是否能單次 grep 命中</td>
      </tr>
      <tr>
          <td>Cross-link 健康度</td>
          <td>引用是否指向正確卡片</td>
          <td><code>_index.md</code> 索引條是否導向同一個概念入口</td>
      </tr>
      <tr>
          <td>反例 / 邊界</td>
          <td>對照段是否保留原因與適用範圍</td>
          <td>標題若使用對照句型，是否有正文立即承接其原因</td>
      </tr>
  </tbody>
</table>
<p>Surface enumeration 讓「我有跑正向陳述 pass」變成可驗證動作，而不只是抽象自我宣告。</p>
<h3 id="第三步用-grep-補字面層掃描">第三步：用 grep 補字面層掃描</h3>
<p>正向陳述是語意判斷，但第一層候選可以用 grep 找出：</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">rg -n <span class="s2">&#34;不行|不可以|不是|不要|無法|不能&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  content/backend/07-security-data-protection/security-as-risk-routing-system.md <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  content/backend/07-security-data-protection/_index.md</span></span></code></pre></div><p>grep 命中代表「需要判讀」，不代表自動違規。合法的對照句型要回到 <a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據</a> 的判準：有正向錨點、有對照原因、有適用情境。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="讀者入口會先傳遞舊-frame">讀者入口會先傳遞舊 frame</h3>
<p>Title 與 MOC hook 是讀者先看到的文字。正文即使已經建立正向概念，入口若仍用負向 hook，讀者第一個 mental model 仍會被帶回「排除某種做法」而非「建立某個責任」。入口 frame 會影響後續閱讀方式。</p>
<h3 id="search-surface-會保留錯誤概念錨點">Search surface 會保留錯誤概念錨點</h3>
<p>Title、description、link label 是搜尋結果與 grep 最容易命中的位置。metadata surface 沒跑 grep-ability 與 positive wording，錯誤概念會比正文更容易被找到，長期變成知識庫中的主要入口。</p>
<h3 id="review-報告會產生-coverage-illusion">Review 報告會產生 coverage illusion</h3>
<p>只寫「已跑 positive wording pass」但沒有列 surface，review 報告會暗示整篇文章已覆蓋。實際上只掃 body surface，metadata surface 仍是未驗證區。這是 <a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass scope 要蓋同類風險區</a> 在單檔內的同形問題。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></strong>：#83 定義 frame 軸，本卡補 surface 軸。Frame 回答「用什麼眼睛看」，surface 回答「哪些文字都要被看」。</li>
<li><strong><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review 的 scope 要蓋同類風險區</a></strong>：#95 處理跨檔 scope，本卡處理單檔內 surface scope。兩者組合成完整 coverage：file scope × surface scope。</li>
<li><strong><a href="../applicability-scope-must-be-enumerated/">#96 適用範圍要展開成 file enumeration</a></strong>：#96 要求可重現的 file list，本卡要求每個 file 內的 surface list。File enumeration 完成後，還要做 surface enumeration。</li>
<li><strong><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據</a></strong>：#94 保留合法對照的推理，本卡定義對照句型出現在 title / MOC hook 時的檢查位置與承接責任。</li>
<li><strong><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></strong>：Title、slug、link label 都是命名。它們需要多輪迭代，在生成後持續用 grep-ability 與讀者入口角度收斂。</li>
<li><strong><a href="../single-source-of-truth/">#44 Single Source of Truth</a></strong>：正文核心概念與 metadata surface 需要共享同一個概念 SSoT。入口文字與正文語意分裂時，讀者會看到兩個 competing source。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>當你完成文章或卡片後，看到以下訊號就要補 surface enumeration：</p>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>正文改成正向概念，title 仍使用排除式 hook</td>
          <td>Metadata surface 漏跑語氣 pass</td>
      </tr>
      <tr>
          <td><code>_index.md</code> 索引條只是沿用第一版標題</td>
          <td>Navigation surface 漏跑對意圖</td>
      </tr>
      <tr>
          <td>Frontmatter description 比正文更像行銷標語</td>
          <td>Search surface 漏跑概念錨點</td>
      </tr>
      <tr>
          <td>Review 紀錄只寫「已檢查文章」但沒列 title / MOC</td>
          <td>Coverage 欠缺驗證依據</td>
      </tr>
      <tr>
          <td>Grep 掃正文通過，搜尋結果仍命中舊句型</td>
          <td>Grep scope 沒包含 metadata</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：技術文章、report 卡片、知識卡片、README、規格文件、skill reference、MOC / <code>_index.md</code></li>
<li><strong>特別適用</strong>：有 frontmatter、sidebar title、SEO description、index table、link label 的內容系統</li>
<li><strong>邊界</strong>：Metadata surface review 是寫作 pass；它需要語意判讀，grep 只負責提出候選</li>
<li><strong>例外</strong>：短訊息、一次性草稿、私人 scratch note 可以只保留 title / body 的最小 surface；production 內容與公開知識庫需要全 surface review</li>
</ul>
<hr>
<h2 id="可操作檢查">可操作檢查</h2>
<p>Production 內容交付前，至少跑這三步：</p>
<ol>
<li>列出這次新增 / 修改檔案的 surface：<code>title</code>、<code>description</code>、heading、body、link label、MOC row。</li>
<li>跑負向詞候選 grep，逐一判讀是否有正向錨點與對照原因。</li>
<li>對照 <code>_index.md</code> 或 MOC，確認索引條、文章標題與正文第一段都指向同一個核心責任。</li>
</ol>
]]></content:encoded></item><item><title>素材庫比例要支撐主情境的反向驗證</title><link>https://tarrragon.github.io/blog/report/source-library-ratio-supports-scenario-validation/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/source-library-ratio-supports-scenario-validation/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>文章主情境負責教推演，素材庫負責支撐反向驗證。&lt;/strong> 當一組文章只展示 4-5 個主情境時，素材庫應保留約 2-3 倍的 field cases 或 source cards，讓每個主情境背後至少有 2-3 個來源可回查。&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>4-5 個&lt;/td>
 &lt;td>讓讀者看完後能實際演練或套用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Field cases / source cards&lt;/td>
 &lt;td>10-12 張&lt;/td>
 &lt;td>支撐情境、反向驗證、補壓力變體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scenario cards&lt;/td>
 &lt;td>4-5 張&lt;/td>
 &lt;td>把多個來源轉成可重播的中性情境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pattern cards&lt;/td>
 &lt;td>5-7 張&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="warp-分析摘要">WARP 分析摘要&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>觀察&lt;/td>
 &lt;td>資安文章只需要 4-5 個推演情境，但 field cases 初始只有 5 張，每個 scenario 大多只靠 1 個來源支撐。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>判讀&lt;/td>
 &lt;td>文章可讀性與素材可信度是兩個不同目標。文章展示情境需要少量精選，素材庫需要較多來源來支撐反向驗證與壓力變體。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>策略&lt;/td>
 &lt;td>採「少情境、多素材」比例：scenario 維持 4-5 張，field/source 素材擴到 10-12 張，pattern 維持 5-7 張。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結論&lt;/td>
 &lt;td>素材庫比例要服務驗證與延伸，而非跟文章情境一比一對齊。source-first 是案例型素材的前置條件。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向驗證：素材庫持續擴張也有成本。當每個主情境已有 2-3 個來源、每個 pattern 有 1-2 個支撐案例時，下一步應轉向寫文章、跑 review 或回寫 MOC。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>在擴充資安章節時，主文章已經有足夠規模：紅隊案例庫、藍隊章節、素材庫與推演情境都已成形。下一步要決定「是否還要繼續補素材」。&lt;/p>
&lt;p>當時有兩個看似相近的問題：&lt;/p>
&lt;ol>
&lt;li>文章讀者實際會看到幾個模擬情境？&lt;/li>
&lt;li>作者要保留多少真實案例作為支撐素材？&lt;/li>
&lt;/ol>
&lt;p>如果把兩者合併，容易得到「文章只用 4-5 個情境，所以素材也只要 4-5 個案例」的結論。這個比例對文章篇幅合理，對反向驗證偏薄。每個情境只有一個來源時，情境會跟單一事件強綁，後續很難判斷哪些是共通壓力、哪些只是個案細節。&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>讀者能否讀完並實際演練&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>Pattern 數&lt;/td>
 &lt;td>是否有跨情境共用做法&lt;/td>
 &lt;td>少於素材，多於單篇文章需要&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="第二步先補來源再寫情境">第二步：先補來源，再寫情境&lt;/h3>
&lt;p>Field case 的責任是保存可回溯材料。案例型素材先找來源，再抽出觀察、壓力、控制缺口、判讀訊號與可轉譯情境。&lt;/p>
&lt;p>Scenario card 的責任是把來源轉譯成可演練情境。情境可以合成多個來源的壓力點，但每個主要壓力都要能回查到 field case 或 source card。&lt;/p>
&lt;p>Pattern card 的責任是歸納。模式可以比單一案例更抽象，但要保留支撐來源、適用邊界、判讀訊號與下一步路由。&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>每個主情境已有 2-3 個來源&lt;/td>
 &lt;td>寫 scenario 或主文章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>每個 pattern 有 1-2 個支撐案例&lt;/td>
 &lt;td>寫 pattern 卡或回寫文章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>新來源只重複既有壓力&lt;/td>
 &lt;td>留入候選清單，先推進文章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文章主線開始被素材清單淹沒&lt;/td>
 &lt;td>收斂 MOC，只保留路由與 hook&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="情境會被單一案例綁死">情境會被單一案例綁死&lt;/h3>
&lt;p>每個情境只靠一個來源時，作者很容易把個案細節當成共通結構。讀者需要拿到的是可搬到自己服務裡的中性情境，而非單一事件改寫。&lt;/p>
&lt;h3 id="pattern-抽象層級會偏高">Pattern 抽象層級會偏高&lt;/h3>
&lt;p>只有單一案例支撐的 pattern 容易過早抽象。作者會把一個事件中的做法升級成通用模式，缺少第二個來源檢查它是否真的跨情境成立。&lt;/p>
&lt;h3 id="後續章節缺少延伸材料">後續章節缺少延伸材料&lt;/h3>
&lt;p>文章當下可完成，但下一輪要寫 incident response、deployment、reliability 或治理節奏時，會重新查資料。素材庫比例不足會讓每次擴寫都回到同一個蒐集成本。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review&lt;/a>&lt;/strong>：#83 說寫作要用多輪 frame，本卡補「素材量也要支撐反向驗證 frame」。只有一個來源時，反例 / 邊界 pass 會缺材料。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review 的 scope 要蓋同類風險區&lt;/a>&lt;/strong>：#95 要求 review scope 蓋同類風險區，本卡把同一精神放到素材庫：source scope 要覆蓋主情境的變體區，並支撐文章會展示的少數案例。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../applicability-scope-must-be-enumerated/">#96 適用範圍要展開成 file enumeration&lt;/a>&lt;/strong>：#96 要求 scope 具體列出，本卡要求素材庫比例具體列出：主情境數、source card 數、scenario 數、pattern 數。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/strong>：素材與文章一比一最方便，但跟「可驗證、可延伸」的意圖不對齊。補足 2-3 倍來源是較高成本但更貼合目標的做法。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉&lt;/a>&lt;/strong>：情境數量是字面層，情境是否有足夠變體支撐是行為層。本卡要求用來源覆蓋度判斷推演素材是否足夠。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍&lt;/a>&lt;/strong>：#97 補 surface 軸，本卡補 material 軸。文章品質不只看正文與入口，也看背後來源是否支撐得住推演。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>看到以下訊號，就要檢查素材庫比例：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>文章主情境負責教推演，素材庫負責支撐反向驗證。</strong> 當一組文章只展示 4-5 個主情境時，素材庫應保留約 2-3 倍的 field cases 或 source cards，讓每個主情境背後至少有 2-3 個來源可回查。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>建議數量</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主文章情境</td>
          <td>4-5 個</td>
          <td>讓讀者看完後能實際演練或套用</td>
      </tr>
      <tr>
          <td>Field cases / source cards</td>
          <td>10-12 張</td>
          <td>支撐情境、反向驗證、補壓力變體</td>
      </tr>
      <tr>
          <td>Scenario cards</td>
          <td>4-5 張</td>
          <td>把多個來源轉成可重播的中性情境</td>
      </tr>
      <tr>
          <td>Pattern cards</td>
          <td>5-7 張</td>
          <td>抽出跨情境共用的做法與判讀欄位</td>
      </tr>
  </tbody>
</table>
<p>比例設計的判別問題是：「<strong>每個主情境背後是否有足夠來源，能支撐反例、變體與下一輪擴寫？</strong>」</p>
<hr>
<h2 id="warp-分析摘要">WARP 分析摘要</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觀察</td>
          <td>資安文章只需要 4-5 個推演情境，但 field cases 初始只有 5 張，每個 scenario 大多只靠 1 個來源支撐。</td>
      </tr>
      <tr>
          <td>判讀</td>
          <td>文章可讀性與素材可信度是兩個不同目標。文章展示情境需要少量精選，素材庫需要較多來源來支撐反向驗證與壓力變體。</td>
      </tr>
      <tr>
          <td>策略</td>
          <td>採「少情境、多素材」比例：scenario 維持 4-5 張，field/source 素材擴到 10-12 張，pattern 維持 5-7 張。</td>
      </tr>
      <tr>
          <td>結論</td>
          <td>素材庫比例要服務驗證與延伸，而非跟文章情境一比一對齊。source-first 是案例型素材的前置條件。</td>
      </tr>
  </tbody>
</table>
<p>反向驗證：素材庫持續擴張也有成本。當每個主情境已有 2-3 個來源、每個 pattern 有 1-2 個支撐案例時，下一步應轉向寫文章、跑 review 或回寫 MOC。</p>
<hr>
<h2 id="情境">情境</h2>
<p>在擴充資安章節時，主文章已經有足夠規模：紅隊案例庫、藍隊章節、素材庫與推演情境都已成形。下一步要決定「是否還要繼續補素材」。</p>
<p>當時有兩個看似相近的問題：</p>
<ol>
<li>文章讀者實際會看到幾個模擬情境？</li>
<li>作者要保留多少真實案例作為支撐素材？</li>
</ol>
<p>如果把兩者合併，容易得到「文章只用 4-5 個情境，所以素材也只要 4-5 個案例」的結論。這個比例對文章篇幅合理，對反向驗證偏薄。每個情境只有一個來源時，情境會跟單一事件強綁，後續很難判斷哪些是共通壓力、哪些只是個案細節。</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>讀者能否讀完並實際演練</td>
          <td>少量、精選、可串成完整流程</td>
      </tr>
      <tr>
          <td>素材庫數</td>
          <td>情境是否有足夠來源支撐</td>
          <td>來源多於情境，支撐變體與反例</td>
      </tr>
      <tr>
          <td>Pattern 數</td>
          <td>是否有跨情境共用做法</td>
          <td>少於素材，多於單篇文章需要</td>
      </tr>
  </tbody>
</table>
<h3 id="第二步先補來源再寫情境">第二步：先補來源，再寫情境</h3>
<p>Field case 的責任是保存可回溯材料。案例型素材先找來源，再抽出觀察、壓力、控制缺口、判讀訊號與可轉譯情境。</p>
<p>Scenario card 的責任是把來源轉譯成可演練情境。情境可以合成多個來源的壓力點，但每個主要壓力都要能回查到 field case 或 source card。</p>
<p>Pattern card 的責任是歸納。模式可以比單一案例更抽象，但要保留支撐來源、適用邊界、判讀訊號與下一步路由。</p>
<h3 id="第三步用停止條件控制素材庫膨脹">第三步：用停止條件控制素材庫膨脹</h3>
<p>素材庫需要明確停止條件。停止條件要寫在大綱中，讓作者知道何時從蒐集轉向寫作。</p>
<table>
  <thead>
      <tr>
          <th>停止條件</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每個主情境已有 2-3 個來源</td>
          <td>寫 scenario 或主文章</td>
      </tr>
      <tr>
          <td>每個 pattern 有 1-2 個支撐案例</td>
          <td>寫 pattern 卡或回寫文章</td>
      </tr>
      <tr>
          <td>新來源只重複既有壓力</td>
          <td>留入候選清單，先推進文章</td>
      </tr>
      <tr>
          <td>文章主線開始被素材清單淹沒</td>
          <td>收斂 MOC，只保留路由與 hook</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="情境會被單一案例綁死">情境會被單一案例綁死</h3>
<p>每個情境只靠一個來源時，作者很容易把個案細節當成共通結構。讀者需要拿到的是可搬到自己服務裡的中性情境，而非單一事件改寫。</p>
<h3 id="pattern-抽象層級會偏高">Pattern 抽象層級會偏高</h3>
<p>只有單一案例支撐的 pattern 容易過早抽象。作者會把一個事件中的做法升級成通用模式，缺少第二個來源檢查它是否真的跨情境成立。</p>
<h3 id="後續章節缺少延伸材料">後續章節缺少延伸材料</h3>
<p>文章當下可完成，但下一輪要寫 incident response、deployment、reliability 或治理節奏時，會重新查資料。素材庫比例不足會讓每次擴寫都回到同一個蒐集成本。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></strong>：#83 說寫作要用多輪 frame，本卡補「素材量也要支撐反向驗證 frame」。只有一個來源時，反例 / 邊界 pass 會缺材料。</li>
<li><strong><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review 的 scope 要蓋同類風險區</a></strong>：#95 要求 review scope 蓋同類風險區，本卡把同一精神放到素材庫：source scope 要覆蓋主情境的變體區，並支撐文章會展示的少數案例。</li>
<li><strong><a href="../applicability-scope-must-be-enumerated/">#96 適用範圍要展開成 file enumeration</a></strong>：#96 要求 scope 具體列出，本卡要求素材庫比例具體列出：主情境數、source card 數、scenario 數、pattern 數。</li>
<li><strong><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></strong>：素材與文章一比一最方便，但跟「可驗證、可延伸」的意圖不對齊。補足 2-3 倍來源是較高成本但更貼合目標的做法。</li>
<li><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></strong>：情境數量是字面層，情境是否有足夠變體支撐是行為層。本卡要求用來源覆蓋度判斷推演素材是否足夠。</li>
<li><strong><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></strong>：#97 補 surface 軸，本卡補 material 軸。文章品質不只看正文與入口，也看背後來源是否支撐得住推演。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>看到以下訊號，就要檢查素材庫比例：</p>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每個 scenario 只靠 1 個 field case</td>
          <td>反向驗證材料偏薄</td>
      </tr>
      <tr>
          <td>Pattern card 只引用單一案例</td>
          <td>抽象層級可能過早</td>
      </tr>
      <tr>
          <td>文章情境讀起來像某事件改名</td>
          <td>中性轉譯材料不足</td>
      </tr>
      <tr>
          <td>後續章節每次都重新查同類案例</td>
          <td>素材庫沒有承擔延伸責任</td>
      </tr>
      <tr>
          <td>MOC 列出大量素材但沒有分層</td>
          <td>素材庫需要比例與路由設計</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：案例驅動文章、事故復盤、資安推演、架構決策素材庫、研究型技術文章、跨多篇 knowledge base</li>
<li><strong>特別適用</strong>：文章只展示少數情境，但需要真實案例支撐可信度與反向驗證</li>
<li><strong>邊界</strong>：比例是設計準則，實際數量依主題風險、來源密度與文章規模調整</li>
<li><strong>停止條件</strong>：每個主情境已有 2-3 個來源、每個 pattern 有 1-2 個支撐案例後，先寫文章與 review</li>
</ul>
<hr>
<h2 id="可操作檢查">可操作檢查</h2>
<p>建立案例型素材庫時，先跑這份檢查：</p>
<ol>
<li>列出主文章情境數，控制在 4-5 個。</li>
<li>為每個主情境配置 2-3 個 field cases 或 source cards。</li>
<li>檢查每張 scenario card 是否能回查來源。</li>
<li>檢查每張 pattern card 是否至少有 1-2 個支撐案例。</li>
<li>在 MOC 寫明停止條件，避免素材蒐集無限延伸。</li>
</ol>
]]></content:encoded></item><item><title>資安教學的審查標準要對應風險不對稱</title><link>https://tarrragon.github.io/blog/report/security-teaching-rigor-asymmetry/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/security-teaching-rigor-asymmetry/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安教學內容的 audit 標準不該由「讀者讀不讀得懂」決定、該由「讀者照做後系統會不會出破口」決定。&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>一般工程教學（layout / refactor / debug）&lt;/td>
 &lt;td>讀者學不會、要重學&lt;/td>
 &lt;td>學習端&lt;/td>
 &lt;td>可逆（再學一次）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資安教學（auth / crypto / 防護 / 標準引用）&lt;/td>
 &lt;td>讀者&lt;strong>以為&lt;/strong>學會、實作時留破口&lt;/td>
 &lt;td>生產端&lt;/td>
 &lt;td>&lt;strong>不可逆&lt;/strong>（破口被利用）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>級數不對稱的後果：一般教學的 audit bar 是「讀者能不能拿到 reasoning」、資安教學的 audit bar 必須升級為「讀者照做後的實作可不可被驗證為無破口」。預設讀者會 implement、不只 read。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安章節（&lt;code>backend/07-security-data-protection/&lt;/code>）的內容形態不是純概念說明、是「問題節點 + 判讀訊號 + 風險後果 + 前置控制面 + 交接路由」。讀者拿到的不只是知識、是&lt;strong>會拿去做防護設計的依據&lt;/strong>。&lt;/p>
&lt;p>寫作便利的選擇（在一般教學沒問題、在資安教學會出事）：&lt;/p>
&lt;ul>
&lt;li>用「能擋」「能防」「可以避免」這類動詞、沒寫適用 threat model&lt;/li>
&lt;li>給防護方法、沒寫「這方法擋不到什麼」&lt;/li>
&lt;li>引用 OWASP / RFC / NIST、沒寫版本 / 沒驗證引用句意&lt;/li>
&lt;li>描述判讀訊號、沒給訊號失效的 deployment 條件&lt;/li>
&lt;li>把 mitigation 寫得通用、沒拆 context-dependence（同 mitigation 在 SaaS / on-prem / 多租戶條件失效不同）&lt;/li>
&lt;/ul>
&lt;p>這些選擇在一般教學是「簡潔風格」、在資安教學是 &lt;strong>silent 破口&lt;/strong>——讀者照字面理解去實作、產生 false sense of security（見 &lt;a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security 是資安寫作的主要失敗模式&lt;/a>）。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>把資安內容的審查標準從 &lt;strong>readability-first&lt;/strong> 升級到 &lt;strong>verifiability-first&lt;/strong>：每個論述要回答「讀者照做後、實作的正確性能不能被反向驗證」。&lt;/p>
&lt;h3 id="三條-audit-bar">三條 audit bar&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Threat model 對稱性&lt;/strong>：講「防 X」必須寫「不防 Y」、形成對稱論述（見 &lt;a href="../threat-model-explicitness/">#101 threat-model-explicitness&lt;/a>）&lt;/li>
&lt;li>&lt;strong>Mitigation 對位驗證&lt;/strong>：防護措施跟 threat 的對應鏈要可驗證、不能只是「業界常用」（見 &lt;a href="../mitigation-threat-alignment/">#102 mitigation-threat-alignment&lt;/a>）&lt;/li>
&lt;li>&lt;strong>Context-dependence 顯式化&lt;/strong>：mitigation 在不同 deployment 的有效性差異要寫出來、不假設讀者知道（見 &lt;a href="../mitigation-context-dependence/">#103 mitigation-context-dependence&lt;/a>）&lt;/li>
&lt;/ol>
&lt;h3 id="寫作流程的差異">寫作流程的差異&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>一般教學&lt;/th>
 &lt;th>資安教學&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>草稿&lt;/td>
 &lt;td>寫得通、有 reasoning&lt;/td>
 &lt;td>+ 列 threat model 範圍 + 列「不在範圍內的 threat」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review&lt;/td>
 &lt;td>多 pass review（&lt;a href="../writing-multi-pass-review/">#83&lt;/a>）&lt;/td>
 &lt;td>+ audit pass（reviewer 視角找 false sense of security）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>引用&lt;/td>
 &lt;td>引用即可&lt;/td>
 &lt;td>+ 標版本 + 驗證引用句意沒被扭曲 + 確認當前版本仍是 best practice&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;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="false-confidence-在生產系統累積">False confidence 在生產系統累積&lt;/h3>
&lt;p>讀者讀完含糊論述、心理上覺得「學到防護方法了」、實作時用最直覺的詮釋。當實作有 gap 時、讀者&lt;strong>不會警覺&lt;/strong>——因為「我學過了 / 我做了」。等同 &lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉&lt;/a> 在資安領域的具體展現：教學含糊 = hook 規則太粗、看似有保護、實際抓不到行為層的破口。&lt;/p>
&lt;p>最危險的是：含糊的資安教學比沒讀過更糟。沒讀過的人會去查標準、會問人；讀過含糊版的人會跳過驗證、直接 implement。&lt;/p>
&lt;h3 id="破口的可利用窗口跟教學擴散同步放大">破口的可利用窗口跟教學擴散同步放大&lt;/h3>
&lt;p>含糊的資安內容若被多個團隊 / 文章引用、所有下游 implementer 都繼承同一個 silent gap。攻擊者只要找到原始教學的 misinterpretation pattern、就可以批量利用所有 implementation。一般教學的錯誤是 &lt;strong>個別讀者的學習成本&lt;/strong>、資安教學的錯誤是 &lt;strong>系統性風險面擴大&lt;/strong>。&lt;/p>
&lt;h3 id="後續修補無法-trace-到原文">後續修補無法 trace 到原文&lt;/h3>
&lt;p>當下游事故發生、回溯到「讀者照某教學實作」時、含糊的原文難以判定是「教學錯」還是「讀者誤解」——因為含糊本身就是 ambiguity 來源。理想的資安教學應該讓「實作 vs 教學」可以被 1:1 對照、出問題時找得到 root cause。&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>&lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉&lt;/a>&lt;/td>
 &lt;td>&lt;strong>本卡是 #82 在資安寫作的領域具體化&lt;/strong> — false confidence 透過含糊教學在實作端展現、是 #82 ceiling pattern 的高風險版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../visual-tool-error-layer-alignment/">#92 視覺手段對齊錯誤層次&lt;/a>&lt;/td>
 &lt;td>&lt;strong>層次錯位 sibling&lt;/strong> — #92 是「呈現工具 vs 內容層次」、本卡是「審查標準 vs 內容風險」、同骨不同維度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>資安寫作最便利（通用敘述 / 省略邊界 / 不標版本）跟意圖對齊（精確 threat / boundary / 標準）反向、本卡是 #67 在資安領域的具體展現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../incremental-shipping-criteria/">#76 分批 ship&lt;/a>&lt;/td>
 &lt;td>&lt;strong>反面對照&lt;/strong> — #76 的三軸切分（可見性 / 風險 / 驗證）適合可逆內容；資安錯誤是不可逆 / 系統層、分批 ship 邏輯不適用、要在 ship 前就把 audit 跑完&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse&lt;/a>&lt;/td>
 &lt;td>「教 X 防護方法」單軸描述是把 threat model 多維度 collapse 成 1 維、跟 #80 同骨——資安教學預設要保留多維度（防什麼 / 不防什麼 / 在哪些 deployment 條件）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../layered-strategy-signal-consistency/">#90 L1+L2 訊號一致性&lt;/a>&lt;/td>
 &lt;td>Silent fallback 即 false confidence、本卡是同類議題在「教學跟實作」之間的一致性問題、訊號要對齊讀者實作端&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>章節用「能擋」「能防」「可以避免」、後面沒接 threat model 範圍&lt;/td>
 &lt;td>補對稱論述：寫「擋 X」也寫「不擋 Y」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>引用 OWASP / RFC / NIST、沒標版本 / 年份&lt;/td>
 &lt;td>補版本標記 + 確認該版本仍是 current best practice&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mitigation 寫得通用、沒拆 deployment 條件&lt;/td>
 &lt;td>補 context-dependence、列 deployment 變數對 mitigation 有效性的影響&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>章節結束讀者會說「我學會 X 防護了」、但你不知道他實作會不會出錯&lt;/td>
 &lt;td>Audit bar 還停在 readability、要升級到 verifiability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「之後讀者實作有疑問再說」&lt;/td>
 &lt;td>是 &lt;a href="../external-trigger-for-high-roi-work/">#72&lt;/a> 結構性跳過、補 audit trigger&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>標題 / index hook 用通用詞（「資安最佳實踐」「防護方法」）、正文寫得精準&lt;/td>
 &lt;td>Metadata surface 漏判（&lt;a href="../metadata-surface-in-writing-review/">#97&lt;/a>）、入口層的含糊會讓正文精準度被誤導&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：資安內容（auth / crypto / 傳輸 / 機敏資料 / 標準引用 / mitigation 設計）、以及任何「讀者照做後錯誤是不可逆 / 系統層」的高風險領域（例：concurrency correctness、distributed consistency claims、financial / medical 計算）&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：純概念說明文章（沒有讀者會直接照做的 step）、實驗性 / playful 內容（讀者預期自行驗證）&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：「verifiability-first」≠「百科全書化」——不是把所有邊界都寫滿、是讓 audit 標準對應風險量級、必要時引用更深的標準文件而不重述&lt;/li>
&lt;li>&lt;strong>過度應用反例&lt;/strong>：把每個資安句子都加滿 boundary / threat / context 補述、文章變密度爆炸、讀者讀不下去——audit bar 對應風險量級、低風險段落（背景介紹 / 概念 anchor）保持簡潔、把 verifiability 投資集中在 mitigation / 標準引用 / 實作 step 段落&lt;/li>
&lt;/ul>
&lt;p>本卡是後續資安 audit 系列卡片（#100-105）的 anchor、確立「為什麼資安寫作需要學術級審查標準」的論證基底。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安教學內容的 audit 標準不該由「讀者讀不讀得懂」決定、該由「讀者照做後系統會不會出破口」決定。</strong> 讀懂是學習端的成本、破口是生產端的代價、兩者級數不同。</p>
<table>
  <thead>
      <tr>
          <th>教學類型</th>
          <th>寫不清楚的代價</th>
          <th>代價發生位置</th>
          <th>可逆性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般工程教學（layout / refactor / debug）</td>
          <td>讀者學不會、要重學</td>
          <td>學習端</td>
          <td>可逆（再學一次）</td>
      </tr>
      <tr>
          <td>資安教學（auth / crypto / 防護 / 標準引用）</td>
          <td>讀者<strong>以為</strong>學會、實作時留破口</td>
          <td>生產端</td>
          <td><strong>不可逆</strong>（破口被利用）</td>
      </tr>
  </tbody>
</table>
<p>級數不對稱的後果：一般教學的 audit bar 是「讀者能不能拿到 reasoning」、資安教學的 audit bar 必須升級為「讀者照做後的實作可不可被驗證為無破口」。預設讀者會 implement、不只 read。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安章節（<code>backend/07-security-data-protection/</code>）的內容形態不是純概念說明、是「問題節點 + 判讀訊號 + 風險後果 + 前置控制面 + 交接路由」。讀者拿到的不只是知識、是<strong>會拿去做防護設計的依據</strong>。</p>
<p>寫作便利的選擇（在一般教學沒問題、在資安教學會出事）：</p>
<ul>
<li>用「能擋」「能防」「可以避免」這類動詞、沒寫適用 threat model</li>
<li>給防護方法、沒寫「這方法擋不到什麼」</li>
<li>引用 OWASP / RFC / NIST、沒寫版本 / 沒驗證引用句意</li>
<li>描述判讀訊號、沒給訊號失效的 deployment 條件</li>
<li>把 mitigation 寫得通用、沒拆 context-dependence（同 mitigation 在 SaaS / on-prem / 多租戶條件失效不同）</li>
</ul>
<p>這些選擇在一般教學是「簡潔風格」、在資安教學是 <strong>silent 破口</strong>——讀者照字面理解去實作、產生 false sense of security（見 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security 是資安寫作的主要失敗模式</a>）。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>把資安內容的審查標準從 <strong>readability-first</strong> 升級到 <strong>verifiability-first</strong>：每個論述要回答「讀者照做後、實作的正確性能不能被反向驗證」。</p>
<h3 id="三條-audit-bar">三條 audit bar</h3>
<ol>
<li><strong>Threat model 對稱性</strong>：講「防 X」必須寫「不防 Y」、形成對稱論述（見 <a href="../threat-model-explicitness/">#101 threat-model-explicitness</a>）</li>
<li><strong>Mitigation 對位驗證</strong>：防護措施跟 threat 的對應鏈要可驗證、不能只是「業界常用」（見 <a href="../mitigation-threat-alignment/">#102 mitigation-threat-alignment</a>）</li>
<li><strong>Context-dependence 顯式化</strong>：mitigation 在不同 deployment 的有效性差異要寫出來、不假設讀者知道（見 <a href="../mitigation-context-dependence/">#103 mitigation-context-dependence</a>）</li>
</ol>
<h3 id="寫作流程的差異">寫作流程的差異</h3>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>一般教學</th>
          <th>資安教學</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>草稿</td>
          <td>寫得通、有 reasoning</td>
          <td>+ 列 threat model 範圍 + 列「不在範圍內的 threat」</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>多 pass review（<a href="../writing-multi-pass-review/">#83</a>）</td>
          <td>+ audit pass（reviewer 視角找 false sense of security）</td>
      </tr>
      <tr>
          <td>引用</td>
          <td>引用即可</td>
          <td>+ 標版本 + 驗證引用句意沒被扭曲 + 確認當前版本仍是 best practice</td>
      </tr>
      <tr>
          <td>完稿</td>
          <td>讀者讀完能套用</td>
          <td>+ 讀者實作後的正確性可被反向驗證</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="false-confidence-在生產系統累積">False confidence 在生產系統累積</h3>
<p>讀者讀完含糊論述、心理上覺得「學到防護方法了」、實作時用最直覺的詮釋。當實作有 gap 時、讀者<strong>不會警覺</strong>——因為「我學過了 / 我做了」。等同 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 在資安領域的具體展現：教學含糊 = hook 規則太粗、看似有保護、實際抓不到行為層的破口。</p>
<p>最危險的是：含糊的資安教學比沒讀過更糟。沒讀過的人會去查標準、會問人；讀過含糊版的人會跳過驗證、直接 implement。</p>
<h3 id="破口的可利用窗口跟教學擴散同步放大">破口的可利用窗口跟教學擴散同步放大</h3>
<p>含糊的資安內容若被多個團隊 / 文章引用、所有下游 implementer 都繼承同一個 silent gap。攻擊者只要找到原始教學的 misinterpretation pattern、就可以批量利用所有 implementation。一般教學的錯誤是 <strong>個別讀者的學習成本</strong>、資安教學的錯誤是 <strong>系統性風險面擴大</strong>。</p>
<h3 id="後續修補無法-trace-到原文">後續修補無法 trace 到原文</h3>
<p>當下游事故發生、回溯到「讀者照某教學實作」時、含糊的原文難以判定是「教學錯」還是「讀者誤解」——因為含糊本身就是 ambiguity 來源。理想的資安教學應該讓「實作 vs 教學」可以被 1:1 對照、出問題時找得到 root cause。</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>本卡是 #82 在資安寫作的領域具體化</strong> — false confidence 透過含糊教學在實作端展現、是 #82 ceiling pattern 的高風險版本</td>
      </tr>
      <tr>
          <td><a href="../visual-tool-error-layer-alignment/">#92 視覺手段對齊錯誤層次</a></td>
          <td><strong>層次錯位 sibling</strong> — #92 是「呈現工具 vs 內容層次」、本卡是「審查標準 vs 內容風險」、同骨不同維度</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>資安寫作最便利（通用敘述 / 省略邊界 / 不標版本）跟意圖對齊（精確 threat / boundary / 標準）反向、本卡是 #67 在資安領域的具體展現</td>
      </tr>
      <tr>
          <td><a href="../incremental-shipping-criteria/">#76 分批 ship</a></td>
          <td><strong>反面對照</strong> — #76 的三軸切分（可見性 / 風險 / 驗證）適合可逆內容；資安錯誤是不可逆 / 系統層、分批 ship 邏輯不適用、要在 ship 前就把 audit 跑完</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td>「教 X 防護方法」單軸描述是把 threat model 多維度 collapse 成 1 維、跟 #80 同骨——資安教學預設要保留多維度（防什麼 / 不防什麼 / 在哪些 deployment 條件）</td>
      </tr>
      <tr>
          <td><a href="../layered-strategy-signal-consistency/">#90 L1+L2 訊號一致性</a></td>
          <td>Silent fallback 即 false confidence、本卡是同類議題在「教學跟實作」之間的一致性問題、訊號要對齊讀者實作端</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>章節用「能擋」「能防」「可以避免」、後面沒接 threat model 範圍</td>
          <td>補對稱論述：寫「擋 X」也寫「不擋 Y」</td>
      </tr>
      <tr>
          <td>引用 OWASP / RFC / NIST、沒標版本 / 年份</td>
          <td>補版本標記 + 確認該版本仍是 current best practice</td>
      </tr>
      <tr>
          <td>Mitigation 寫得通用、沒拆 deployment 條件</td>
          <td>補 context-dependence、列 deployment 變數對 mitigation 有效性的影響</td>
      </tr>
      <tr>
          <td>章節結束讀者會說「我學會 X 防護了」、但你不知道他實作會不會出錯</td>
          <td>Audit bar 還停在 readability、要升級到 verifiability</td>
      </tr>
      <tr>
          <td>「之後讀者實作有疑問再說」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 audit trigger</td>
      </tr>
      <tr>
          <td>標題 / index hook 用通用詞（「資安最佳實踐」「防護方法」）、正文寫得精準</td>
          <td>Metadata surface 漏判（<a href="../metadata-surface-in-writing-review/">#97</a>）、入口層的含糊會讓正文精準度被誤導</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容（auth / crypto / 傳輸 / 機敏資料 / 標準引用 / mitigation 設計）、以及任何「讀者照做後錯誤是不可逆 / 系統層」的高風險領域（例：concurrency correctness、distributed consistency claims、financial / medical 計算）</li>
<li><strong>不適用</strong>：純概念說明文章（沒有讀者會直接照做的 step）、實驗性 / playful 內容（讀者預期自行驗證）</li>
<li><strong>邊界</strong>：「verifiability-first」≠「百科全書化」——不是把所有邊界都寫滿、是讓 audit 標準對應風險量級、必要時引用更深的標準文件而不重述</li>
<li><strong>過度應用反例</strong>：把每個資安句子都加滿 boundary / threat / context 補述、文章變密度爆炸、讀者讀不下去——audit bar 對應風險量級、低風險段落（背景介紹 / 概念 anchor）保持簡潔、把 verifiability 投資集中在 mitigation / 標準引用 / 實作 step 段落</li>
</ul>
<p>本卡是後續資安 audit 系列卡片（#100-105）的 anchor、確立「為什麼資安寫作需要學術級審查標準」的論證基底。</p>
]]></content:encoded></item><item><title>False sense of security 是資安寫作的主要失敗模式</title><link>https://tarrragon.github.io/blog/report/false-sense-of-security-as-primary-failure/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/false-sense-of-security-as-primary-failure/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安教學的主要失敗模式不是「讀者學不到」、是「讀者以為學到了」。&lt;/strong> 學不到是 active failure（讀者知道自己沒會、會去查）、以為學到是 silent failure（讀者跳過驗證、直接 implement、破口在生產系統累積）。&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;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>以為學會&lt;/strong>&lt;/td>
 &lt;td>不知道自己沒會&lt;/td>
 &lt;td>跳過驗證、直接 implement&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>安全 baseline 達成&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>中間那行（false sense of security）是資安寫作要消滅的目標。&lt;strong>比沒讀過更糟&lt;/strong>——沒讀過會去查，讀過含糊版會跳過。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>讀者讀完資安章節、會自然形成一個結論：「我學到了 X 防護方法」。這個結論的安全性依賴它能不能被分解成可驗證的子句：&lt;/p>
&lt;ul>
&lt;li>對什麼 threat 安全？&lt;/li>
&lt;li>在什麼 deployment 條件下成立？&lt;/li>
&lt;li>什麼前提失效時這個防護失效？&lt;/li>
&lt;li>跟既有實作疊加會不會 silent 干擾？&lt;/li>
&lt;/ul>
&lt;p>如果讀者讀完無法回答這四題、結論就是空殼——表面上「學到了」、實質上是 false sense of security。資安章節（&lt;code>backend/07-security-data-protection/&lt;/code>）的「問題節點」表格容易長出這個結構：&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">前置控制面：authentication / incident-severity&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>讀者讀完知道「節奏失衡很危險」、但&lt;strong>不知道&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>「節奏失衡」具體閾值是什麼？（threat model 沒寫）&lt;/li>
&lt;li>「authentication」是哪一層的 control？適用什麼 deployment？（context-dependence 沒寫）&lt;/li>
&lt;li>用了 control 之後、什麼情況下還是會擴散？（mitigation 邊界沒寫）&lt;/li>
&lt;/ul>
&lt;p>讀者照字面實作 → 心裡覺得「節奏控管做了、authentication 用了」→ silent gap。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>把「讀者讀完會說什麼」當成 audit 主軸。對每段論述跑這個反向驗證：&lt;/p>
&lt;h3 id="反向驗證模板">反向驗證模板&lt;/h3>
&lt;p>寫完一段、自問：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>讀者讀完會在心裡形成什麼結論？&lt;/strong>（例：「我做了 session invalidation 就安全」）&lt;/li>
&lt;li>&lt;strong>這個結論能不能拆成可驗證子句？&lt;/strong>（對什麼攻擊安全 / 什麼條件下 / 什麼前提失效）&lt;/li>
&lt;li>&lt;strong>如果不能、補哪一塊讓它能？&lt;/strong>（threat model / context / boundary / 前提條件）&lt;/li>
&lt;/ol>
&lt;p>走完三步、原文若仍是「讀完會 false confidence」、必須改寫——加 contrast、加 boundary、加前提、或拆成更小的可驗證單元。&lt;/p>
&lt;h3 id="識別-false-sense-句子的訊號詞">識別 false-sense 句子的訊號詞&lt;/h3>
&lt;p>下列詞彙在資安內容是 high-risk、預設要被 audit：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號詞&lt;/th>
 &lt;th>為什麼是 risk&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「能擋」「能防」「可避免」&lt;/td>
 &lt;td>沒指定擋什麼、預設讀者會自行補完整 threat space、實際只擋作者腦中的 subset&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「最佳實踐」「業界標準」&lt;/td>
 &lt;td>隱含 universal validity、跳過 context-dependence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「使用 X 即可」&lt;/td>
 &lt;td>把 mitigation 當成銀彈、跳過邊界跟疊加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「業界常用」「常見做法」&lt;/td>
 &lt;td>Appeal to convention、不是 mitigation 對位驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「應該足夠」「通常足夠」&lt;/td>
 &lt;td>沒給「足夠」的定義、讀者會用最寬鬆詮釋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「有效」「有用」&lt;/td>
 &lt;td>對什麼 threat 有效？讀者預設「對所有」、實際只對 subset&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每出現一個訊號詞、檢查段落有沒有對應的 boundary 補述；沒有 → 補完或改寫。&lt;/p>
&lt;h3 id="methodology-layer-訊號詞多-layer-false-sense">Methodology-layer 訊號詞（多 layer false sense）&lt;/h3>
&lt;p>False sense of security 不只發生在 mitigation layer——還會在 &lt;strong>methodology / framework / process layer&lt;/strong> 出現。Reader 讀完「我們有方法論 / 有路由系統 / 有 maturity stage / 有 release gate / 有 tripwire」、形成「&lt;strong>有 system / framework = 安全&lt;/strong>」結論、跳過驗證下游 control 是否真擋 threat。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Layer&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;th>Reader 形成的 false 結論&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Mitigation layer&lt;/td>
 &lt;td>上一張表訊號詞&lt;/td>
 &lt;td>「我做了 X mitigation 就安全」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Methodology layer&lt;/td>
 &lt;td>把 framework / routing 當成已治理 risk&lt;/td>
 &lt;td>「我們有 routing system / framework 了 = 風險可控」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Process layer&lt;/td>
 &lt;td>把 gate / checklist 當成 risk reduce&lt;/td>
 &lt;td>「跑了 release gate / 例外有 tripwire = 安全」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Maturity layer&lt;/td>
 &lt;td>把 stage 等級當成 mitigation 強度&lt;/td>
 &lt;td>「我們在可稽核閉環 stage = 風險低」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Methodology-layer 訊號詞清單：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安教學的主要失敗模式不是「讀者學不到」、是「讀者以為學到了」。</strong> 學不到是 active failure（讀者知道自己沒會、會去查）、以為學到是 silent failure（讀者跳過驗證、直接 implement、破口在生產系統累積）。</p>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>讀者狀態</th>
          <th>後續行為</th>
          <th>系統端後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>讀不懂</td>
          <td>知道自己沒會</td>
          <td>去查標準 / 問人 / 重學</td>
          <td>學習延遲、實作前找補</td>
      </tr>
      <tr>
          <td><strong>以為學會</strong></td>
          <td>不知道自己沒會</td>
          <td>跳過驗證、直接 implement</td>
          <td><strong>生產破口、事件偵測前無人警覺</strong></td>
      </tr>
      <tr>
          <td>讀懂並會驗證</td>
          <td>知道邊界、知道何時失效</td>
          <td>實作 + 持續驗證</td>
          <td>安全 baseline 達成</td>
      </tr>
  </tbody>
</table>
<p>中間那行（false sense of security）是資安寫作要消滅的目標。<strong>比沒讀過更糟</strong>——沒讀過會去查，讀過含糊版會跳過。</p>
<hr>
<h2 id="情境">情境</h2>
<p>讀者讀完資安章節、會自然形成一個結論：「我學到了 X 防護方法」。這個結論的安全性依賴它能不能被分解成可驗證的子句：</p>
<ul>
<li>對什麼 threat 安全？</li>
<li>在什麼 deployment 條件下成立？</li>
<li>什麼前提失效時這個防護失效？</li>
<li>跟既有實作疊加會不會 silent 干擾？</li>
</ul>
<p>如果讀者讀完無法回答這四題、結論就是空殼——表面上「學到了」、實質上是 false sense of security。資安章節（<code>backend/07-security-data-protection/</code>）的「問題節點」表格容易長出這個結構：</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">前置控制面：authentication / incident-severity</span></span></code></pre></div><p>讀者讀完知道「節奏失衡很危險」、但<strong>不知道</strong>：</p>
<ul>
<li>「節奏失衡」具體閾值是什麼？（threat model 沒寫）</li>
<li>「authentication」是哪一層的 control？適用什麼 deployment？（context-dependence 沒寫）</li>
<li>用了 control 之後、什麼情況下還是會擴散？（mitigation 邊界沒寫）</li>
</ul>
<p>讀者照字面實作 → 心裡覺得「節奏控管做了、authentication 用了」→ silent gap。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>把「讀者讀完會說什麼」當成 audit 主軸。對每段論述跑這個反向驗證：</p>
<h3 id="反向驗證模板">反向驗證模板</h3>
<p>寫完一段、自問：</p>
<ol>
<li><strong>讀者讀完會在心裡形成什麼結論？</strong>（例：「我做了 session invalidation 就安全」）</li>
<li><strong>這個結論能不能拆成可驗證子句？</strong>（對什麼攻擊安全 / 什麼條件下 / 什麼前提失效）</li>
<li><strong>如果不能、補哪一塊讓它能？</strong>（threat model / context / boundary / 前提條件）</li>
</ol>
<p>走完三步、原文若仍是「讀完會 false confidence」、必須改寫——加 contrast、加 boundary、加前提、或拆成更小的可驗證單元。</p>
<h3 id="識別-false-sense-句子的訊號詞">識別 false-sense 句子的訊號詞</h3>
<p>下列詞彙在資安內容是 high-risk、預設要被 audit：</p>
<table>
  <thead>
      <tr>
          <th>訊號詞</th>
          <th>為什麼是 risk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「能擋」「能防」「可避免」</td>
          <td>沒指定擋什麼、預設讀者會自行補完整 threat space、實際只擋作者腦中的 subset</td>
      </tr>
      <tr>
          <td>「最佳實踐」「業界標準」</td>
          <td>隱含 universal validity、跳過 context-dependence</td>
      </tr>
      <tr>
          <td>「使用 X 即可」</td>
          <td>把 mitigation 當成銀彈、跳過邊界跟疊加</td>
      </tr>
      <tr>
          <td>「業界常用」「常見做法」</td>
          <td>Appeal to convention、不是 mitigation 對位驗證</td>
      </tr>
      <tr>
          <td>「應該足夠」「通常足夠」</td>
          <td>沒給「足夠」的定義、讀者會用最寬鬆詮釋</td>
      </tr>
      <tr>
          <td>「有效」「有用」</td>
          <td>對什麼 threat 有效？讀者預設「對所有」、實際只對 subset</td>
      </tr>
  </tbody>
</table>
<p>每出現一個訊號詞、檢查段落有沒有對應的 boundary 補述；沒有 → 補完或改寫。</p>
<h3 id="methodology-layer-訊號詞多-layer-false-sense">Methodology-layer 訊號詞（多 layer false sense）</h3>
<p>False sense of security 不只發生在 mitigation layer——還會在 <strong>methodology / framework / process layer</strong> 出現。Reader 讀完「我們有方法論 / 有路由系統 / 有 maturity stage / 有 release gate / 有 tripwire」、形成「<strong>有 system / framework = 安全</strong>」結論、跳過驗證下游 control 是否真擋 threat。</p>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>失敗模式</th>
          <th>Reader 形成的 false 結論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mitigation layer</td>
          <td>上一張表訊號詞</td>
          <td>「我做了 X mitigation 就安全」</td>
      </tr>
      <tr>
          <td>Methodology layer</td>
          <td>把 framework / routing 當成已治理 risk</td>
          <td>「我們有 routing system / framework 了 = 風險可控」</td>
      </tr>
      <tr>
          <td>Process layer</td>
          <td>把 gate / checklist 當成 risk reduce</td>
          <td>「跑了 release gate / 例外有 tripwire = 安全」</td>
      </tr>
      <tr>
          <td>Maturity layer</td>
          <td>把 stage 等級當成 mitigation 強度</td>
          <td>「我們在可稽核閉環 stage = 風險低」</td>
      </tr>
  </tbody>
</table>
<p>Methodology-layer 訊號詞清單：</p>
<table>
  <thead>
      <tr>
          <th>訊號詞</th>
          <th>為什麼是 risk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「有方法論」「有 framework」</td>
          <td>方法論不擋 threat、是把 threat 路由到對應控制面、實際擋 threat 仍靠下游控制</td>
      </tr>
      <tr>
          <td>「能路由」「decision system」「分類治理」</td>
          <td>Routing 提供分類、不提供 mitigation；reader 不點下游控制可能停留在 routing layer</td>
      </tr>
      <tr>
          <td>「有 maturity stage / process」</td>
          <td>Maturity 是 process metric、不等於 risk reduction；mature process 可在某 deployment 條件下 silent 失效</td>
      </tr>
      <tr>
          <td>「跑了 gate / 過了 checklist」</td>
          <td>Gate / checklist 通過 ≠ control 真擋 threat、可能是 ceremonial false sense（<a href="../literal-interception-vs-behavioral-refinement/">#82</a> 字面層）</td>
      </tr>
      <tr>
          <td>「設了 tripwire」「有重評估機制」</td>
          <td>Tripwire 沒 quantify（threshold / cadence / owner）等同沒設、見 <a href="../escalation-trigger-quantification/">#91</a></td>
      </tr>
      <tr>
          <td>「能治理」「可控」「閉環」</td>
          <td>治理 / 閉環是流程語、reader 預設「閉環 = 風險擋住」、實際閉環只是流程 cycle、不保證 mitigation 強度</td>
      </tr>
  </tbody>
</table>
<p>驗證方式跟 mitigation layer 同：reader 讀完能否拆 falsifiable 子句？能不能列出<strong>具體下游 control + 各自 boundary + 各自驗證訊號</strong>？不能 → methodology-layer false sense 產地、補「下一步路由 / 必連控制面 / 各 control 的 verification check」。</p>
<h3 id="對抗只給結論的句法">對抗「只給結論」的句法</h3>
<p>跟 <a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a> 同骨：資安結論單獨成立會空降、必須跟 contrast / 邊界 / 前提同句承載。</p>
<table>
  <thead>
      <tr>
          <th>危險寫法</th>
          <th>安全寫法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 HTTPS 保護傳輸」</td>
          <td>「使用 HTTPS 防中間人讀取、不防 endpoint 信任失效（CA compromise / cert pinning bypass）」</td>
      </tr>
      <tr>
          <td>「JWT 用簽章驗證身分」</td>
          <td>「簽章驗 token 沒被竄改、不驗 token 沒被竊取（XSS / 明文存儲）、需配 rotation + 短 TTL」</td>
      </tr>
      <tr>
          <td>「rate limit 擋 brute force」</td>
          <td>「per-IP rate limit 擋單來源連續嘗試、不擋分散來源（botnet / credential stuffing）」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="silent-failure-比-noisy-failure-更貴">Silent failure 比 noisy failure 更貴</h3>
<p>Noisy failure（讀者讀不懂、實作報錯、被 reviewer 抓到）發生在開發前期、修復成本是 commit 等級。silent failure（讀者以為對了、ship 進生產）發生在生產系統、可能等到事件才被發現、修復成本跳到事件處理 + 通報 + 復盤 + 信任修復。</p>
<p>跟 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 同病——#82 的核心是「驗證工具的字面層 vs 行為層 ceiling」：CI 字面層通過不代表行為層沒問題、但 CI 通過會建立 false confidence、阻止後續行為層檢查。本卡是 #82 在資安寫作的具體展現：含糊的論述提供字面 mitigation、讀者讀完建立 false confidence、阻止實作端的行為層 verify。</p>
<h3 id="教學擴散把單篇-silent-gap-放大成系統性-risk">教學擴散把單篇 silent gap 放大成系統性 risk</h3>
<p>含糊的資安內容若被多團隊引用 / 翻譯 / 二次教材化、原始 misinterpretation pattern 會被批量繼承。攻擊者只需找一次 misinterpretation、就可以利用所有 implementation。一般教學的錯誤是個別讀者的學習成本、資安教學的錯誤是 risk surface 集體放大——跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a> 在資安領域的展現：寫得越輕鬆、擴散越快、silent gap 越廣。</p>
<h3 id="事故發生後的-root-cause-無法還原">事故發生後的 root cause 無法還原</h3>
<p>下游事件 root cause 分析時、若實作來源是含糊的教學內容、無法判定是「教學錯」還是「讀者誤解」——含糊本身就是 ambiguity 來源、責任邊界模糊。理想的資安內容應該能讓「實作 vs 教學」1:1 對照、出問題時 trace 得到 root cause（<a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 的 traceability 面）。</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>本卡是 #82 的領域具體化最危險版本</strong> — false confidence 在資安寫作的展現、後果不可逆、是 #82 ceiling pattern 的高風險案例</td>
      </tr>
      <tr>
          <td><a href="../layered-strategy-signal-consistency/">#90 L1+L2 訊號一致性</a></td>
          <td><strong>同骨 sibling</strong> — silent fallback 即 false confidence、本卡是同類議題在「教學跟實作之間訊號一致」的展現</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td><strong>#99 的下游主軸</strong> — #99 立論「為什麼資安要學術級 audit」、本卡定義「audit 主要要找什麼」、99 → 100 是動機 → 目標的因果鏈</td>
      </tr>
      <tr>
          <td><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a></td>
          <td>同骨：刪掉 contrast 讓結論空降、本卡的「只給防護不給邊界」是同病在資安領域的展現</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>含糊敘述是寫作最便利選擇、跟「讓讀者實作正確」反向、本卡是 #67 在 silent failure 維度的展現</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td>「我學會 X 防護了」是把多維度（threat / context / boundary）collapse 成 1 bit、跟 #80 同骨——資安結論預設保留多維度、不能 collapse</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段落出現「能擋」「能防」「最佳實踐」「即可」</td>
          <td>預設高風險、檢查有沒有對應 boundary 補述</td>
      </tr>
      <tr>
          <td>讀者讀完會說「我做了 X 就安全」</td>
          <td>結論無法拆可驗證子句、補 threat / context / boundary / 前提</td>
      </tr>
      <tr>
          <td>Mitigation 寫得乾淨、沒有 contrast</td>
          <td>跟 <a href="../positive-rewrite-preserves-contrast/">#94</a> 同病、補對照論據</td>
      </tr>
      <tr>
          <td>引用標準（OWASP / RFC / NIST）但不寫版本</td>
          <td>假設標準 universal、補版本 + 適用條件</td>
      </tr>
      <tr>
          <td>「業界常用 / 常見做法」當論證</td>
          <td>Appeal to convention、補 mitigation 對位驗證</td>
      </tr>
      <tr>
          <td>章節結束讀者覺得「都涵蓋了」、但你列不出涵蓋邊界</td>
          <td>入口層 false confidence、補 metadata surface（<a href="../metadata-surface-in-writing-review/">#97</a>）</td>
      </tr>
      <tr>
          <td>「之後實作時應該會發現問題」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 audit trigger</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容（auth / crypto / 防護 / 標準引用 / mitigation 設計）的 audit；任何「讀者照做後錯誤是不可逆 / 系統層」的高風險領域（concurrency 正確性、distributed consistency claims、financial / medical 計算）</li>
<li><strong>不適用</strong>：純概念說明 / 歷史背景內容（讀者不會直接照做）、研究探討文章（讀者預期自行驗證）</li>
<li><strong>邊界</strong>：「消滅 false sense of security」≠「把所有邊界寫到極致」——是讓讀者讀完能列出邊界、不是讓讀者讀完什麼都不敢做。Audit bar 是 verifiability、不是完備性</li>
<li><strong>過度警覺反例</strong>：對所有句子都打防呆 disclaimer、把資安內容寫成 legal-style 「在 X 條件下、若無 Y 前提、且不考慮 Z 攻擊路徑、可能可以」——讀者讀不到任何 actionable 結論、退化成「什麼都不要做」式 paranoia、跟 silent failure 一樣有害；判別準則：讀者讀完應該能列出<strong>可實作的 mitigation 集合 + 各自 boundary</strong>、不是「不知道該不該做任何事」</li>
</ul>
<p>本卡是後續資安 audit 維度卡片（#101-104）的主軸——每個維度都在回答「false sense of security 在哪裡產生」。</p>
]]></content:encoded></item><item><title>Threat model 明確性：「防什麼」與「不防什麼」必須對稱</title><link>https://tarrragon.github.io/blog/report/threat-model-explicitness/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/threat-model-explicitness/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>寫資安 mitigation 必須對稱：每段「防什麼」要配「不防什麼」、不能單邊寫。&lt;/strong> 讀者沒拿到「不防 Y」、會用 universal validity 詮釋 mitigation——預設「防 X」涵蓋整個 threat space、實際只是 X 的 subset。Threat model 的 boundary 是 mitigation 論述的一部分、不是補充說明、不能省。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>寫法&lt;/th>
 &lt;th>讀者會形成的結論&lt;/th>
 &lt;th>結論的 scope&lt;/th>
 &lt;th>實作覆蓋率&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「使用 HTTPS 保護傳輸」&lt;/td>
 &lt;td>HTTPS 防傳輸風險&lt;/td>
 &lt;td>全部傳輸風險（universal）&lt;/td>
 &lt;td>subset（中間人 read）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「使用 HTTPS 防中間人讀取、不防 endpoint 信任失效」&lt;/td>
 &lt;td>HTTPS 防 X、不防 Y&lt;/td>
 &lt;td>顯式 scope&lt;/td>
 &lt;td>對應 X、reader 知道補 Y&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差別在於讀者實作時的覆蓋判斷——前者讀完跳過 endpoint 驗證、後者讀完知道要補 Y。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安寫作有兩個誘因會讓 threat model boundary 被省略：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>正向陳述優先&lt;/strong>規範（AGENTS.md 原則二）會誤把「不防 Y」歸類為負面句、批量改寫時刪掉、跟 &lt;a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據&lt;/a> 同病&lt;/li>
&lt;li>&lt;strong>章節篇幅控制&lt;/strong>會把 threat boundary 當成「進階補充」往後丟、留主章節「乾淨主旨」&lt;/li>
&lt;/ol>
&lt;p>兩者都會產出 universal-flavored 的 mitigation 句子。讀者讀「使用 X 即可保護 Y」時、Y 會被腦補成「所有 Y 相關攻擊」、X 跟 Y 之間的 scope 配對被 silent 地放大成 universal。&lt;/p>
&lt;p>實際資安章節（&lt;code>backend/07-security-data-protection/&lt;/code>）會出現的形態：&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">前置控制面：authentication / incident-severity&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個寫法把 threat 抽象成「節奏失衡」、把 mitigation 抽象成「authentication」——對熟手 OK、對學習者讀完會以為「用 authentication 就擋節奏失衡」、實作時不會去問 authentication 的局部 scope（防 credential 弱、不防 session 重放、不防 supply chain 信任傳導）。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>每個 mitigation 句子強制走「對稱論述」結構：&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">[Mitigation X] 防 [in-scope threat A]、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">不防 [out-of-scope threat B]（[B 的補強路由 / 外部引用]）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三個欄位都要填：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>In-scope threat&lt;/strong>：X 真正擋的攻擊類型（具體、不抽象）&lt;/li>
&lt;li>&lt;strong>Out-of-scope threat&lt;/strong>：讀者最容易誤以為 X 也擋的攻擊（讀者直覺會 extrapolate 的方向）&lt;/li>
&lt;li>&lt;strong>補強路由&lt;/strong>：Y 該由什麼補（其他章節 / 外部標準 / 已知條件）、不能只丟「自己想辦法」&lt;/li>
&lt;/ul>
&lt;p>例（HTTPS 章節）：&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">HTTPS 防中間人「讀取」傳輸內容（passive eavesdrop）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">不防 endpoint 「身分驗證」失效（compromised CA / cert pinning bypass）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">endpoint 信任靠 cert pinning + CT log monitoring 補（見 7.5）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例（per-IP rate limit）：&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">per-IP rate limit 擋「單來源」連續嘗試（brute force from single host）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">不擋「分散來源」嘗試（botnet / credential stuffing）、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">分散攻擊靠 reputation-based filtering + adaptive challenge 補（見 7.3）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="對稱不是補負面是scope-顯式化">對稱不是「補負面」、是「scope 顯式化」&lt;/h3>
&lt;p>跟 &lt;a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據&lt;/a> 同骨：「不防 Y」不是負向陳述、是 mitigation 的 scope qualifier。把 contrast 寫進句子、不是違反「正向陳述優先」、是讓主句的 X claim 站得住。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>寫資安 mitigation 必須對稱：每段「防什麼」要配「不防什麼」、不能單邊寫。</strong> 讀者沒拿到「不防 Y」、會用 universal validity 詮釋 mitigation——預設「防 X」涵蓋整個 threat space、實際只是 X 的 subset。Threat model 的 boundary 是 mitigation 論述的一部分、不是補充說明、不能省。</p>
<table>
  <thead>
      <tr>
          <th>寫法</th>
          <th>讀者會形成的結論</th>
          <th>結論的 scope</th>
          <th>實作覆蓋率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 HTTPS 保護傳輸」</td>
          <td>HTTPS 防傳輸風險</td>
          <td>全部傳輸風險（universal）</td>
          <td>subset（中間人 read）</td>
      </tr>
      <tr>
          <td>「使用 HTTPS 防中間人讀取、不防 endpoint 信任失效」</td>
          <td>HTTPS 防 X、不防 Y</td>
          <td>顯式 scope</td>
          <td>對應 X、reader 知道補 Y</td>
      </tr>
  </tbody>
</table>
<p>差別在於讀者實作時的覆蓋判斷——前者讀完跳過 endpoint 驗證、後者讀完知道要補 Y。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安寫作有兩個誘因會讓 threat model boundary 被省略：</p>
<ol>
<li><strong>正向陳述優先</strong>規範（AGENTS.md 原則二）會誤把「不防 Y」歸類為負面句、批量改寫時刪掉、跟 <a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a> 同病</li>
<li><strong>章節篇幅控制</strong>會把 threat boundary 當成「進階補充」往後丟、留主章節「乾淨主旨」</li>
</ol>
<p>兩者都會產出 universal-flavored 的 mitigation 句子。讀者讀「使用 X 即可保護 Y」時、Y 會被腦補成「所有 Y 相關攻擊」、X 跟 Y 之間的 scope 配對被 silent 地放大成 universal。</p>
<p>實際資安章節（<code>backend/07-security-data-protection/</code>）會出現的形態：</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">前置控制面：authentication / incident-severity</span></span></code></pre></div><p>這個寫法把 threat 抽象成「節奏失衡」、把 mitigation 抽象成「authentication」——對熟手 OK、對學習者讀完會以為「用 authentication 就擋節奏失衡」、實作時不會去問 authentication 的局部 scope（防 credential 弱、不防 session 重放、不防 supply chain 信任傳導）。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 mitigation 句子強制走「對稱論述」結構：</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">[Mitigation X] 防 [in-scope threat A]、
</span></span><span class="line"><span class="ln">2</span><span class="cl">不防 [out-of-scope threat B]（[B 的補強路由 / 外部引用]）。</span></span></code></pre></div><p>三個欄位都要填：</p>
<ul>
<li><strong>In-scope threat</strong>：X 真正擋的攻擊類型（具體、不抽象）</li>
<li><strong>Out-of-scope threat</strong>：讀者最容易誤以為 X 也擋的攻擊（讀者直覺會 extrapolate 的方向）</li>
<li><strong>補強路由</strong>：Y 該由什麼補（其他章節 / 外部標準 / 已知條件）、不能只丟「自己想辦法」</li>
</ul>
<p>例（HTTPS 章節）：</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">HTTPS 防中間人「讀取」傳輸內容（passive eavesdrop）、
</span></span><span class="line"><span class="ln">2</span><span class="cl">不防 endpoint 「身分驗證」失效（compromised CA / cert pinning bypass）、
</span></span><span class="line"><span class="ln">3</span><span class="cl">endpoint 信任靠 cert pinning + CT log monitoring 補（見 7.5）。</span></span></code></pre></div><p>例（per-IP rate limit）：</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">per-IP rate limit 擋「單來源」連續嘗試（brute force from single host）、
</span></span><span class="line"><span class="ln">2</span><span class="cl">不擋「分散來源」嘗試（botnet / credential stuffing）、
</span></span><span class="line"><span class="ln">3</span><span class="cl">分散攻擊靠 reputation-based filtering + adaptive challenge 補（見 7.3）。</span></span></code></pre></div><h3 id="對稱不是補負面是scope-顯式化">對稱不是「補負面」、是「scope 顯式化」</h3>
<p>跟 <a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a> 同骨：「不防 Y」不是負向陳述、是 mitigation 的 scope qualifier。把 contrast 寫進句子、不是違反「正向陳述優先」、是讓主句的 X claim 站得住。</p>
<table>
  <thead>
      <tr>
          <th>違反「正向陳述優先」</th>
          <th>符合「正向陳述優先」 + 對稱 boundary</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「不要忘了 X 不防 Y」（命令）</td>
          <td>「X 防 A、不防 B（B 由 C 補）」（陳述）</td>
      </tr>
      <tr>
          <td>「Y 是 X 的限制」（負面 framing）</td>
          <td>「X 的 scope 是 A」（正面 framing）</td>
      </tr>
  </tbody>
</table>
<p>主句仍然承載 X 的 mitigation claim（正向）、不防 Y 是 scope qualifier、不是論述主體——結構符合「正向陳述優先」、語意保留 boundary。</p>
<h3 id="threat-model-的層級對應">Threat model 的層級對應</h3>
<p>對稱論述要在三個層級保持一致：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>對稱 threat model 的形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>章節級</td>
          <td>章節 lead 段列出整體 threat scope + 不在 scope 的 threat 路由</td>
      </tr>
      <tr>
          <td>段落級</td>
          <td>每個 mitigation 段配對應 threat 跟 boundary</td>
      </tr>
      <tr>
          <td>句子級</td>
          <td>「X 防 A、不防 B」單句承載</td>
      </tr>
  </tbody>
</table>
<p>三個層級任一缺、reader 都可能 silent universal 詮釋。實作 audit 時三層分別檢查、不是只看句子。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="reader-用-universal-詮釋實作覆蓋永遠是-worst-case">Reader 用 universal 詮釋、實作覆蓋永遠是 worst case</h3>
<p>人類讀句子時、預設的 scope 是 universal、不是 minimal——這是語言學跟認知偏差的結合。「使用 X 防 Y」讀者預設 X 防整個 Y space。要讓讀者預設 minimal、必須<strong>顯式給 boundary</strong>——這跟物件的 type narrowing 同骨：沒寫 narrowing predicate、預設 widest type。</p>
<p>跟 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 主軸對應——universal 詮釋是 false sense 的主要產地。</p>
<h3 id="reviewer-跟原作者對-mitigation-的-scope-認知會-silent-drift">Reviewer 跟原作者對 mitigation 的 scope 認知會 silent drift</h3>
<p>含糊 threat model 的 mitigation 段、不同 reviewer 讀會 reconstruct 出不同的 in-scope 集合。原作者腦中是 [A, B]、reviewer 讀成 [A, B, C, D]、實作者實作為 [A, B, C, D, E]——三個人對同一段話的覆蓋認知都不同、且都覺得自己對。對稱寫法讓 scope 變成 fact、不是 reconstruction。</p>
<h3 id="多-mitigation-疊加時的-gap-永遠看不到">多 mitigation 疊加時的 gap 永遠看不到</h3>
<p>多個 mitigation 各自寫 in-scope、不寫 out-of-scope、疊加時的 gap（哪個 threat 沒人擋）就看不到。對稱寫法讓每個 mitigation 都有顯式 boundary、疊加 audit 時可以做集合運算（聯集 in-scope 應涵蓋 threat space、否則有 gap）。沒對稱寫法、audit 工具只能憑感覺、無法量化覆蓋。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>本卡是消滅 #100 的具體 dimension 1</strong> — universal 詮釋是 false sense 的主要產地、對稱論述是直接的 mitigation</td>
      </tr>
      <tr>
          <td><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a></td>
          <td><strong>同骨 sibling</strong> — #94 在寫作規範執行層、本卡在資安寫作層、都在說「contrast 是論述完整性的一部分、不能為了正向化而刪」</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍是 sanity 防線</a></td>
          <td><strong>scope explicitness 同骨</strong> — #43 在 JS 邊界 / selector / observer scope、本卡在 mitigation 的 threat scope、都在說「scope 不顯式 = 失控的 widening」</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — #99 立論「為什麼要 verifiability-first」、本卡是 verifiability 的具體實現之一</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td>「X 防 Y 嗎」的 yes/no 詮釋是 collapse、對稱論述是把多維度（A 防 / B 不防 / 由 C 補）展開回多軸</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mitigation 段只寫 in-scope、沒寫 out-of-scope</td>
          <td>補對稱論述、加 out-of-scope + 補強路由</td>
      </tr>
      <tr>
          <td>「使用 X 防 Y」單句、Y 是抽象詞（傳輸風險 / 身分風險）</td>
          <td>Y 太寬、specific 化 Y 的 in-scope subset、列出 out-of-scope 補 boundary</td>
      </tr>
      <tr>
          <td>章節 lead 段沒列整體 threat scope</td>
          <td>補章節級 threat model、確立 scope qualifier 的 anchor</td>
      </tr>
      <tr>
          <td>多個 mitigation 段並列、各自寫 mitigation、沒寫疊加 gap</td>
          <td>補疊加 audit、聯集 in-scope vs 整體 threat space、找 gap</td>
      </tr>
      <tr>
          <td>把「不防 Y」寫成獨立警告段、跟 mitigation 分開</td>
          <td>對稱論述應該同句承載、分開寫會被讀者跳過或當成「進階補充」</td>
      </tr>
      <tr>
          <td>Reviewer 讀完問「那 Z 攻擊呢？」</td>
          <td>Z 在 reader 直覺 in-scope、原文沒對稱寫、補 Z 為 out-of-scope 並標路由</td>
      </tr>
      <tr>
          <td>「之後讀者實作時會自己想到 boundary」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 audit trigger</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：所有資安 mitigation 論述（auth / crypto / 傳輸 / 防護 / 標準引用）；高風險領域的「方法論述」（concurrency primitive 的 ordering 保證、distributed consensus 的 failure mode）</li>
<li><strong>不適用</strong>：純歷史 / 概念介紹文章（不教 mitigation、不需 threat model）、研究探討（讀者預期自行 explore boundary）</li>
<li><strong>邊界</strong>：「對稱論述」≠「列出所有不防的 threat」——只列讀者直覺會 extrapolate 的方向、不是 enumerate 整個 threat space。判別準則：「讀者讀完 X 防 A 之後、心裡最可能誤以為 X 也防的 B 是什麼？」——B 就是該寫的 out-of-scope</li>
<li><strong>過度對稱反例</strong>：每個 mitigation 列十個 out-of-scope threat、文體變 audit-driven（為了 audit checklist 而寫）、不是 reader-driven（為讓讀者建立可驗證 mental model 而寫）；單一 mitigation 的 out-of-scope 通常 1-2 個直覺 extrapolation 方向就夠、列 10 個 = 把 audit 模板當成寫作目標、退化成 #67 寫作便利度反向</li>
</ul>
<p>本卡是資安 audit 第一個維度（threat model）、配 #102 mitigation 對位、#103 context-dependence、#104 citation 形成完整的 audit dimension 集合。</p>
]]></content:encoded></item><item><title>Mitigation 對位：防護對應到具體 threat 的驗證</title><link>https://tarrragon.github.io/blog/report/mitigation-threat-alignment/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mitigation-threat-alignment/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安 mitigation 對讀者有意義的不是「mitigation 存在」、是「mitigation 跟 threat 的對應鏈成立」。&lt;/strong> 對應鏈拆三段——&lt;code>設計上 mitigation X 攔 threat Y&lt;/code> + &lt;code>攔的 mechanism 是 Z&lt;/code> + &lt;code>Z 失效時的訊號是 W&lt;/code>——任一段空、mitigation 在實作端就會跟 threat 錯位、變成「看似在防、實際只擋表面 artifact」的 defense theater。&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>攔什麼 threat（設計 in-scope）&lt;/td>
 &lt;td>mitigation 變裝飾、讀者實作時不知道測試該擋什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>攔的 mechanism&lt;/td>
 &lt;td>mitigation 對位到 threat 表面 artifact、不是攻擊 mechanism、變體攻擊立刻繞過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失效訊號&lt;/td>
 &lt;td>mitigation 失效時讀者不知道、靠 silent assumption 撐著&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三段都齊、reader 才能反向驗證實作有沒有達到設計強度。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安章節常見的論述形態：給 mitigation 名稱（rate limit、CSRF token、prepared statement）+ 對應 threat 名稱（brute force、CSRF、SQLi）。表面對位、底層 mechanism 沒交代。讀者讀「prepared statement 防 SQLi」、實作時用 string concat + escape function、心裡覺得「我擋 SQLi 了」——因為原文只給 mitigation/threat 對應、沒給 mechanism（parameterization 跟 escape 是兩種不同 mechanism、抗的攻擊面不同）。&lt;/p>
&lt;p>實際 case 的失效模式有三類：&lt;/p>
&lt;h3 id="失效模式-1mitigation-攔表面-artifact不是攻擊-mechanism">失效模式 1：Mitigation 攔表面 artifact、不是攻擊 mechanism&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">論述：rate limit 擋 brute force
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">讀者實作：per-IP rate limit
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">攻擊 mechanism：分散來源（botnet）每個 IP 低頻率、整體高頻率
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">結果：mitigation 攔到的是「單 IP 高頻」表面、不是「身分嘗試」mechanism&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對位該寫的是「rate limit 攔『單來源高頻嘗試』、不攔『身分嘗試』本身」——mechanism level 的對位、不是名稱對位。&lt;/p>
&lt;h3 id="失效模式-2mitigation-跟-threat-在不同抽象層">失效模式 2：Mitigation 跟 threat 在不同抽象層&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">論述：CSP 擋 XSS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">讀者實作：CSP header 設 default-src &amp;#39;self&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">Threat 抽象層：XSS 是 injection class、有 reflected / stored / DOM 三類
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">Mitigation 抽象層：CSP 是 browser-side execution policy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">結果：CSP 擋「未授權 script 執行」、不擋 stored XSS 在 DB 已 persist 的階段&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對位該寫 mitigation 在抽象層的位置——CSP 在 browser 執行層、不在 input 處理層。讀者光看「CSP 擋 XSS」會以為 input sanitization 不必做。&lt;/p>
&lt;h3 id="失效模式-3mitigation-假設上層-threat-已擋">失效模式 3：Mitigation 假設上層 threat 已擋&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">論述：bcrypt 防 password DB 外洩後 brute force 還原
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">讀者實作：bcrypt 存 password
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">被忽略的 threat：DB 外洩前 - phishing / credential stuffing / weak password
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">結果：bcrypt 是「外洩後」的 last line、不是 password security 的 first line&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對位該寫 mitigation 在 defense-in-depth 的層次跟前提——bcrypt 在「&lt;strong>假設&lt;/strong> DB 外洩」的條件下成立、不擋外洩前的 threat。讀者沒拿到前提、會以為 bcrypt 是 password security 的 sufficient solution。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安 mitigation 對讀者有意義的不是「mitigation 存在」、是「mitigation 跟 threat 的對應鏈成立」。</strong> 對應鏈拆三段——<code>設計上 mitigation X 攔 threat Y</code> + <code>攔的 mechanism 是 Z</code> + <code>Z 失效時的訊號是 W</code>——任一段空、mitigation 在實作端就會跟 threat 錯位、變成「看似在防、實際只擋表面 artifact」的 defense theater。</p>
<table>
  <thead>
      <tr>
          <th>對應鏈段落</th>
          <th>缺失時的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>攔什麼 threat（設計 in-scope）</td>
          <td>mitigation 變裝飾、讀者實作時不知道測試該擋什麼</td>
      </tr>
      <tr>
          <td>攔的 mechanism</td>
          <td>mitigation 對位到 threat 表面 artifact、不是攻擊 mechanism、變體攻擊立刻繞過</td>
      </tr>
      <tr>
          <td>失效訊號</td>
          <td>mitigation 失效時讀者不知道、靠 silent assumption 撐著</td>
      </tr>
  </tbody>
</table>
<p>三段都齊、reader 才能反向驗證實作有沒有達到設計強度。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安章節常見的論述形態：給 mitigation 名稱（rate limit、CSRF token、prepared statement）+ 對應 threat 名稱（brute force、CSRF、SQLi）。表面對位、底層 mechanism 沒交代。讀者讀「prepared statement 防 SQLi」、實作時用 string concat + escape function、心裡覺得「我擋 SQLi 了」——因為原文只給 mitigation/threat 對應、沒給 mechanism（parameterization 跟 escape 是兩種不同 mechanism、抗的攻擊面不同）。</p>
<p>實際 case 的失效模式有三類：</p>
<h3 id="失效模式-1mitigation-攔表面-artifact不是攻擊-mechanism">失效模式 1：Mitigation 攔表面 artifact、不是攻擊 mechanism</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">論述：rate limit 擋 brute force
</span></span><span class="line"><span class="ln">2</span><span class="cl">讀者實作：per-IP rate limit
</span></span><span class="line"><span class="ln">3</span><span class="cl">攻擊 mechanism：分散來源（botnet）每個 IP 低頻率、整體高頻率
</span></span><span class="line"><span class="ln">4</span><span class="cl">結果：mitigation 攔到的是「單 IP 高頻」表面、不是「身分嘗試」mechanism</span></span></code></pre></div><p>對位該寫的是「rate limit 攔『單來源高頻嘗試』、不攔『身分嘗試』本身」——mechanism level 的對位、不是名稱對位。</p>
<h3 id="失效模式-2mitigation-跟-threat-在不同抽象層">失效模式 2：Mitigation 跟 threat 在不同抽象層</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">論述：CSP 擋 XSS
</span></span><span class="line"><span class="ln">2</span><span class="cl">讀者實作：CSP header 設 default-src &#39;self&#39;
</span></span><span class="line"><span class="ln">3</span><span class="cl">Threat 抽象層：XSS 是 injection class、有 reflected / stored / DOM 三類
</span></span><span class="line"><span class="ln">4</span><span class="cl">Mitigation 抽象層：CSP 是 browser-side execution policy
</span></span><span class="line"><span class="ln">5</span><span class="cl">結果：CSP 擋「未授權 script 執行」、不擋 stored XSS 在 DB 已 persist 的階段</span></span></code></pre></div><p>對位該寫 mitigation 在抽象層的位置——CSP 在 browser 執行層、不在 input 處理層。讀者光看「CSP 擋 XSS」會以為 input sanitization 不必做。</p>
<h3 id="失效模式-3mitigation-假設上層-threat-已擋">失效模式 3：Mitigation 假設上層 threat 已擋</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">論述：bcrypt 防 password DB 外洩後 brute force 還原
</span></span><span class="line"><span class="ln">2</span><span class="cl">讀者實作：bcrypt 存 password
</span></span><span class="line"><span class="ln">3</span><span class="cl">被忽略的 threat：DB 外洩前 - phishing / credential stuffing / weak password
</span></span><span class="line"><span class="ln">4</span><span class="cl">結果：bcrypt 是「外洩後」的 last line、不是 password security 的 first line</span></span></code></pre></div><p>對位該寫 mitigation 在 defense-in-depth 的層次跟前提——bcrypt 在「<strong>假設</strong> DB 外洩」的條件下成立、不擋外洩前的 threat。讀者沒拿到前提、會以為 bcrypt 是 password security 的 sufficient solution。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 mitigation 段落補三欄對位：</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">[Mitigation X]
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 攔的 threat：[具體攻擊行為、不是攻擊類別名稱]
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 攔的 mechanism：[X 在什麼層擋 / 擋的是 mechanism 的哪一步]
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 失效訊號：[reader 能觀察到 mitigation 有沒有發揮的具體現象]</span></span></code></pre></div><p>例（per-IP rate limit）：</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">per-IP rate limit
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 攔的 threat：單來源連續嘗試（同 IP 短時間多次 login）
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 攔的 mechanism：在 single-source 維度限制 attempt rate、攻擊者必須切 IP 才能繞
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 失效訊號：分散來源（多 IP 各自低頻）的 aggregate 嘗試率、per-IP rate limit metric 不會 trigger</span></span></code></pre></div><p>例（bcrypt password hashing）：</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">bcrypt
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 攔的 threat：DB 外洩後 password 被離線 brute force 還原
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 攔的 mechanism：work factor 控制 hash 計算成本、攻擊者每次嘗試的成本不可優化
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 失效訊號：weak password / 已知 password 在 dictionary 中、攻擊者不需 brute force 全 space
</span></span><span class="line"><span class="ln">5</span><span class="cl">- 前提：上層擋住 phishing / credential stuffing、bcrypt 是 last line、不是 first line</span></span></code></pre></div><h3 id="對位的層次規則">對位的層次規則</h3>
<p>對位驗證要在三個層次都對齊：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>對位的形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>名稱層</td>
          <td>mitigation 名稱 → threat 名稱（最弱、容易裝飾）</td>
      </tr>
      <tr>
          <td>Mechanism 層</td>
          <td>mitigation 擋的攻擊 mechanism → threat 的具體 mechanism</td>
      </tr>
      <tr>
          <td>前提層</td>
          <td>mitigation 成立的前提 → 前提失效時的 fallback / upstream control</td>
      </tr>
  </tbody>
</table>
<p>只到名稱層 = defense theater；到 mechanism 層 = 可實作驗證；到前提層 = 可疊加 defense-in-depth audit。</p>
<h3 id="對位-audit-的工具方法">對位 audit 的工具方法</h3>
<p>對 mitigation 群組做集合運算：</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. 列章節所有 mitigation 跟對應 threat
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 對每對 (mitigation, threat) 補 mechanism + 前提
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 集合化：聯集所有 mitigation 攔的 mechanism、聯集所有 threat 的 mechanism
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 找 gap：threat 集合裡沒被 mitigation 集合涵蓋的 mechanism
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. Gap 處理：補 mitigation / 標 out-of-scope（[#101](../threat-model-explicitness/)）/ 升級到 defense-in-depth 上層</span></span></code></pre></div><p>集合運算讓對位錯誤跟覆蓋 gap 從「靠感覺」升級到「可量化」。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="defense-theater-在-audit-跟-implementation-都通過生產系統有破口">Defense theater 在 audit 跟 implementation 都通過、生產系統有破口</h3>
<p>只到名稱層對位的 mitigation、audit 工具看到「rate limit 已部署」會 pass、implementation 看到「CSRF token 已加」會 pass、threat 還在——攻擊者用 mechanism 變體（分散來源 / DOM XSS / stored injection）繞過、mitigation 集體 silent 失效。<strong>對位錯誤的 mitigation 跟沒 mitigation 在攻擊者眼中等價、但對 audit / 對讀者不等價</strong>——這個 gap 是 defense theater 的本質。</p>
<p>跟 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 同骨：mitigation 名稱對位是字面層、mechanism 對位是行為層、前提對位是 contextual 行為層。stop at 字面層 = false confidence。</p>
<h3 id="mitigation-變體跟-threat-變體無法-trace">Mitigation 變體跟 threat 變體無法 trace</h3>
<p>新 threat 出現（如 credential stuffing 之於傳統 brute force）、reader 必須重新評估既有 mitigation 是否還對位。對位鏈寫到 mechanism + 前提的 mitigation 可被 trace（per-IP rate limit 的 mechanism 是 single-source 限制、credential stuffing 是分散來源、不對位、需新 mitigation）；只到名稱層的 mitigation 不可 trace（rate limit vs credential stuffing：名稱看起來「應該擋」、實際不擋）。寫作時的 mechanism / 前提投資、是給未來 threat evolution 留的 review 入口。</p>
<h3 id="mitigation-疊加時的責任邊界含糊">Mitigation 疊加時的責任邊界含糊</h3>
<p>多個 mitigation 共防一個 threat、若各自不寫 mechanism + 前提、疊加時無法判斷「誰負責什麼層」。修補某個 mitigation 時不知道會不會影響其他 mitigation 的前提、變更冒險成本上升。明示 mechanism + 前提 = 明示 mitigation 之間的 dependency、修補成本可控。</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>本卡是 #82 在 mitigation 設計層的具體化</strong> — mitigation 名稱對位 = 字面層、mechanism 對位 = 行為層、前提對位 = contextual 行為層；stop at 名稱層 = false confidence</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層對策階梯</a></td>
          <td><strong>同骨對位邏輯</strong> — #86 是 capability gap 的 L1/L2/L3 對應；本卡是 mitigation 在「名稱 / mechanism / 前提」三層對應；都在說「層次選對才有效」</td>
      </tr>
      <tr>
          <td><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強策略</a></td>
          <td><strong>疊加 mitigation 的對位</strong> — #75 是多策略疊加判準（解不同層 / 沒副作用衝突 / 增量成本可接受），本卡補「疊加時各 mitigation 的 mechanism 跟前提要明示」、不然 #75 的判準沒法跑</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>#100 的 dimension 2</strong> — 對位失效是 false sense 的第二大產地（dimension 1 是 threat model 不對稱、見 <a href="../threat-model-explicitness/">#101</a>）</td>
      </tr>
      <tr>
          <td><a href="../threat-model-explicitness/">#101 Threat model 明確性</a></td>
          <td><strong>本卡的上游前提</strong> — #101 確立 threat space 的 scope、本卡確立 mitigation 在 scope 內的 mechanism 對位；threat model 不清的話 mitigation 對位無從談起</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — verifiability-first 的具體實現之二（#101 是 dimension 1、本卡是 dimension 2）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mitigation 段只寫「X 防 Y」、沒寫 mechanism</td>
          <td>補 mechanism 層：X 在什麼抽象層擋、擋的是 Y 的哪一步</td>
      </tr>
      <tr>
          <td>Mitigation 用 threat 類別名稱（brute force / SQLi / XSS）對位</td>
          <td>類別名稱太寬、specific 化到具體攻擊行為（單 IP 高頻 / payload boundary / stored vs reflected）</td>
      </tr>
      <tr>
          <td>Mitigation 段沒寫前提、讀者不知道何時失效</td>
          <td>補前提層：mitigation 在什麼條件下成立、條件失效時的 upstream control</td>
      </tr>
      <tr>
          <td>多個 mitigation 並列、各自寫對應、沒寫疊加 dependency</td>
          <td>補集合運算 audit、聯集 mechanism 集合 vs 整體 threat space</td>
      </tr>
      <tr>
          <td>Reviewer 讀完問「這跟 [新 threat 變體] 對到嗎？」</td>
          <td>對位鏈停在名稱層、補 mechanism 讓變體可被 trace</td>
      </tr>
      <tr>
          <td>「業界常用 X 防 Y」當論證</td>
          <td>Appeal to convention、補 mechanism 對位驗證、不能用「常用」代替</td>
      </tr>
      <tr>
          <td>章節開頭列 threat、結尾列 mitigation、中間沒對位鏈</td>
          <td>補對位段、把兩個列表 link 成 (threat, mitigation, mechanism, 前提) 表</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安 mitigation 設計（auth / crypto / 防護 / 標準 control）的論述；任何「方法 → 問題」對應的高 stakes 領域（concurrency primitive 對 race 類別 / consensus 演算法對 failure mode / financial control 對 risk 類別）</li>
<li><strong>不適用</strong>：純概念 / 歷史介紹（不教 mitigation）、研究探討（讀者預期自行 explore mechanism）</li>
<li><strong>邊界</strong>：「對位驗證」≠「列出每個攻擊變體」——mechanism 層列到攻擊行為的根 mechanism 即可、不必列所有 surface 變體；判別準則是「reader 能不能用這個 mechanism 描述去判斷新變體攻擊是否在 mitigation 覆蓋內」</li>
<li><strong>過度對位反例</strong>：每個 mitigation 寫 mechanism + 前提 + 三層 scope qualifier + 五種失效訊號、文章變 audit checklist、不是教學；mitigation 對位的投資量級對應 mitigation 在系統的責任比重——核心 mitigation（auth / crypto primitive）值得三層完整對位、輔助 mitigation（log redaction / banner notice）只到 mechanism 層即可</li>
</ul>
<p>本卡是資安 audit 第二個維度（mitigation 對位）、配 <a href="../threat-model-explicitness/">#101</a> threat model 明確性、後續 #103 context-dependence、#104 citation 形成完整 audit dimension 集合。</p>
]]></content:encoded></item><item><title>Mitigation 的 context-dependence：deployment 條件改變有效性</title><link>https://tarrragon.github.io/blog/report/mitigation-context-dependence/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mitigation-context-dependence/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安 mitigation 的有效性不是 mitigation 本身決定的、是 mitigation × deployment 條件決定的。&lt;/strong> 同一個 mitigation 在不同 deployment / config / scale / runtime 條件下、強度光譜從「完整擋」到「等同沒部署」都可能。寫作時忽略 deployment 變數、讀者實作時用最直覺條件詮釋、實際部署條件不對 mitigation silent 失效。&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>「使用 X 保護 Y」（universal-flavored）&lt;/td>
 &lt;td>在「正常」條件下 X 防 Y&lt;/td>
 &lt;td>條件不對、X silent 失效、無人警覺&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「使用 X 保護 Y、條件 Z」&lt;/td>
 &lt;td>條件 Z 成立才用 X、否則補 W&lt;/td>
 &lt;td>條件不對時 reader 知道補 W&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差別在於：reader 在實作 review 階段有沒有 context 變數可檢查。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安 mitigation 在文獻 / 標準 / 教學裡常被描述成「方法 → 防什麼 threat」對應、跳過 deployment 條件這個變數。讀者讀完套到自己 deployment 上、條件可能不一致。常見的 context dimension 有四類：&lt;/p>
&lt;h3 id="context-維度-1config-完整性">Context 維度 1：Config 完整性&lt;/h3>
&lt;p>Mitigation 通常需要多個 config 同時成立才有效、單一 config 不夠：&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">HTTPS 防中間人：成立條件 = TLS + HSTS + cert pinning（針對重要 endpoint）+ CT log monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 失效條件 = 只有 TLS、沒 HSTS → 第一次連線可被 downgrade
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> 沒 cert pinning → 受信任 CA 簽出假 cert 可繞過
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">JWT 驗身分： 成立條件 = 簽章驗證 + 短 TTL + rotation + 安全儲存（HttpOnly cookie 或 secure storage）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> 失效條件 = 簽章對但 TTL 太長 → token 被竊後長期可用
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> XSS 可讀取 → 簽章保護被繞過
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> 沒 rotation → 一次外洩永久暴露&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寫「使用 HTTPS」「使用 JWT」是把 mitigation 縮成單一 control name、reader 預設 default config、實際要 5-7 個 config 同時對才完整。&lt;/p>
&lt;h3 id="context-維度-2scale--多實例">Context 維度 2：Scale / 多實例&lt;/h3>
&lt;p>某些 mitigation 在單機 OK、多實例失效：&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">Rate limit： 單實例 = local counter、per-IP rate 控管準確
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> 多實例 = 每實例各自 count、攻擊者打不同實例可繞過 N 倍上限
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> 修法 = 用 distributed counter（Redis / 共享 store）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">Session 失效：單實例 = local session store、invalidate 即時
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> 多實例 = invalidate 訊號需 broadcast、舊 token 在其他實例還可用
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> 修法 = 用 stateless token + revocation list 或 共享 session store&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reader 看到「rate limit 防 brute force」、實作時若不知道 deployment scale、單實例 OK / 多實例 silent 失效。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安 mitigation 的有效性不是 mitigation 本身決定的、是 mitigation × deployment 條件決定的。</strong> 同一個 mitigation 在不同 deployment / config / scale / runtime 條件下、強度光譜從「完整擋」到「等同沒部署」都可能。寫作時忽略 deployment 變數、讀者實作時用最直覺條件詮釋、實際部署條件不對 mitigation silent 失效。</p>
<table>
  <thead>
      <tr>
          <th>描述形態</th>
          <th>讀者實作判斷</th>
          <th>部署條件不對的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 X 保護 Y」（universal-flavored）</td>
          <td>在「正常」條件下 X 防 Y</td>
          <td>條件不對、X silent 失效、無人警覺</td>
      </tr>
      <tr>
          <td>「使用 X 保護 Y、條件 Z」</td>
          <td>條件 Z 成立才用 X、否則補 W</td>
          <td>條件不對時 reader 知道補 W</td>
      </tr>
  </tbody>
</table>
<p>差別在於：reader 在實作 review 階段有沒有 context 變數可檢查。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安 mitigation 在文獻 / 標準 / 教學裡常被描述成「方法 → 防什麼 threat」對應、跳過 deployment 條件這個變數。讀者讀完套到自己 deployment 上、條件可能不一致。常見的 context dimension 有四類：</p>
<h3 id="context-維度-1config-完整性">Context 維度 1：Config 完整性</h3>
<p>Mitigation 通常需要多個 config 同時成立才有效、單一 config 不夠：</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">HTTPS 防中間人：成立條件 = TLS + HSTS + cert pinning（針對重要 endpoint）+ CT log monitoring
</span></span><span class="line"><span class="ln">2</span><span class="cl">                失效條件 = 只有 TLS、沒 HSTS → 第一次連線可被 downgrade
</span></span><span class="line"><span class="ln">3</span><span class="cl">                          沒 cert pinning → 受信任 CA 簽出假 cert 可繞過
</span></span><span class="line"><span class="ln">4</span><span class="cl">JWT 驗身分：    成立條件 = 簽章驗證 + 短 TTL + rotation + 安全儲存（HttpOnly cookie 或 secure storage）
</span></span><span class="line"><span class="ln">5</span><span class="cl">                失效條件 = 簽章對但 TTL 太長 → token 被竊後長期可用
</span></span><span class="line"><span class="ln">6</span><span class="cl">                          XSS 可讀取 → 簽章保護被繞過
</span></span><span class="line"><span class="ln">7</span><span class="cl">                          沒 rotation → 一次外洩永久暴露</span></span></code></pre></div><p>寫「使用 HTTPS」「使用 JWT」是把 mitigation 縮成單一 control name、reader 預設 default config、實際要 5-7 個 config 同時對才完整。</p>
<h3 id="context-維度-2scale--多實例">Context 維度 2：Scale / 多實例</h3>
<p>某些 mitigation 在單機 OK、多實例失效：</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">Rate limit： 單實例 = local counter、per-IP rate 控管準確
</span></span><span class="line"><span class="ln">2</span><span class="cl">            多實例 = 每實例各自 count、攻擊者打不同實例可繞過 N 倍上限
</span></span><span class="line"><span class="ln">3</span><span class="cl">            修法 = 用 distributed counter（Redis / 共享 store）
</span></span><span class="line"><span class="ln">4</span><span class="cl">Session 失效：單實例 = local session store、invalidate 即時
</span></span><span class="line"><span class="ln">5</span><span class="cl">            多實例 = invalidate 訊號需 broadcast、舊 token 在其他實例還可用
</span></span><span class="line"><span class="ln">6</span><span class="cl">            修法 = 用 stateless token + revocation list 或 共享 session store</span></span></code></pre></div><p>Reader 看到「rate limit 防 brute force」、實作時若不知道 deployment scale、單實例 OK / 多實例 silent 失效。</p>
<h3 id="context-維度-3runtime-環境">Context 維度 3：Runtime 環境</h3>
<p>執行環境差異改變 mitigation 適用性：</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">Cookie SameSite=Strict 防 CSRF：
</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">  Native app webview = 部分有效（依 webview 實作）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  Mobile in-app browser = 不一定有效（看實作）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  Server-to-server = 不適用（無 cookie / 無 SameSite 概念）
</span></span><span class="line"><span class="ln">6</span><span class="cl">CSP 防 XSS：
</span></span><span class="line"><span class="ln">7</span><span class="cl">  Modern browser = 有效
</span></span><span class="line"><span class="ln">8</span><span class="cl">  舊瀏覽器（IE / 非 evergreen）= partial 或無效
</span></span><span class="line"><span class="ln">9</span><span class="cl">  非 browser execution（Electron / native webview）= 看 implementation</span></span></code></pre></div><h3 id="context-維度-4threat-actor-能力">Context 維度 4：Threat actor 能力</h3>
<p>Mitigation 的 work factor 跟 threat actor 計算能力對應：</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">bcrypt（work factor = 10）：
</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">  Nation-state（GPU farm / FPGA）= 弱保護、需提高 work factor 或換 argon2
</span></span><span class="line"><span class="ln">4</span><span class="cl">PBKDF2（100k iterations）：
</span></span><span class="line"><span class="ln">5</span><span class="cl">  2010 年 = 強
</span></span><span class="line"><span class="ln">6</span><span class="cl">  2026 年 = 弱（建議升級到 600k+ 或 argon2）</span></span></code></pre></div><p>Threat actor 能力是 deployment 隨時間變化的變數、寫作時固定描述很快過時。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 mitigation 段落明示三類條件：</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">[Mitigation X]
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 成立條件：[X 發揮設計強度需要的 config / scale / runtime / 其他 control 配套]
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 失效條件：[條件不對時 X 變成 etc 等同沒部署的具體情境]
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Deployment 變數：[實作時要檢查的 dimension list]</span></span></code></pre></div><p>例（rate limit 防 brute force）：</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">per-IP rate limit
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 成立條件：單實例部署 OR 多實例 + distributed counter（Redis / 共享 store）
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 失效條件：多實例 + local counter、攻擊者輪流打不同實例繞過上限
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Deployment 變數：實例數量、counter 部署位置（local / shared）、IP 來源真實性（NAT / proxy 後是否還能 distinguish）</span></span></code></pre></div><p>例（HTTPS 防中間人）：</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">HTTPS
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 成立條件：TLS + HSTS（避免首連線 downgrade）+ 受信 CA chain + 在重要 endpoint 配 cert pinning
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 失效條件：沒 HSTS → 首次連線 downgrade；CA 被攻陷 → 假 cert 可繞；no cert pinning + state-level CA 攻陷 → silent MITM
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Deployment 變數：HSTS preload / max-age 設定、cert pinning 範圍（哪些 endpoint）、CA list 是否最小化、CT log monitoring 是否到位</span></span></code></pre></div><h3 id="context-描述的層次規則">Context 描述的層次規則</h3>
<p>每個 mitigation 描述至少要有 deployment baseline 跟 stretch case：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Baseline 條件</td>
          <td>最常見 deployment（單機 / 標準 config / mainstream browser）下的有效性</td>
      </tr>
      <tr>
          <td>Stretch 條件</td>
          <td>scale / 異常 runtime / 高能力 actor 下的衰減</td>
      </tr>
      <tr>
          <td>Trigger condition</td>
          <td>何時 baseline 不夠、要升級到 stretch 的訊號</td>
      </tr>
  </tbody>
</table>
<p>baseline 給 reader 入門條件、stretch 給 reader 升級判準、trigger 讓升級成 actionable signal。</p>
<h3 id="跟規模改變可行性的同骨">跟「規模改變可行性」的同骨</h3>
<p>跟 <a href="../dataset-scale-changes-feasibility/">#89 Dataset 規模改變什麼可行</a> 同骨——#89 在 dataset / index / cache 維度、本卡在 mitigation / config / scale 維度：</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">#89:    &lt; 1MB 無腦處理 → 1-10MB O(N) 可行 → &gt; 100MB 強制 index
</span></span><span class="line"><span class="ln">2</span><span class="cl">本卡：   單實例 local rate limit OK → 多實例需 distributed counter → 高 scale 需 token bucket + adaptive</span></span></code></pre></div><p>「在 X 規模 / 條件下 Y 方法 OK」這個結構在資料處理跟資安都成立、是 deployment 變數驅動的工程光譜。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="正常條件下有效silent-變成生產破口">「正常條件下有效」silent 變成生產破口</h3>
<p>讀者讀「使用 X 防 Y」、用自己 deployment 的 default config 實作、跑開發測試 OK、ship 進生產。生產可能是多實例 / 高 scale / 異常 runtime、X 在那條件下不成立、threat 進入。<strong>Mitigation 在開發環境 silent 失效、生產環境 silent 失效——兩階段都沒訊號、直到事件</strong>。</p>
<p>跟 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 同病：context 沒寫、reader 用最直覺條件詮釋、condition mismatch 不會被 catch。</p>
<h3 id="mitigation-升級的時機不可-trace">Mitigation 升級的時機不可 trace</h3>
<p>威脅環境變化（actor 計算能力 / 攻擊變體 / scale 增長）需要 mitigation 跟著升級。Context 寫清楚的 mitigation 可 trace（bcrypt work factor 跟 actor 能力對應、定期 review）；context 含糊的 mitigation 不可 trace（「使用 bcrypt」變成 frozen「最佳實踐」、實際強度跟著時間 decay）。</p>
<h3 id="跨環境-deployment-的-mitigation-假設衝突">跨環境 deployment 的 mitigation 假設衝突</h3>
<p>同一份教學 / spec 套到不同 deployment（dev / staging / prod / 多區域 / 不同租戶）、若 context 沒寫、各 deployment 的 mitigation 強度差異被 silent。Audit 跨 deployment 時無法判定哪個強度最弱、整個系統的 baseline 取決於最弱 deployment、但沒人知道哪個是最弱。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../dataset-scale-changes-feasibility/">#89 Dataset 規模改變什麼可行</a></td>
          <td><strong>同骨 sibling</strong> — #89 是「資料規模 → 處理方法可行性」、本卡是「deployment 條件 → mitigation 有效性」、都是「條件變數驅動的方法光譜」</td>
      </tr>
      <tr>
          <td><a href="../build-time-vs-runtime-computation-spectrum/">#87 Build-time vs Runtime 計算光譜</a></td>
          <td><strong>同骨 spectrum</strong> — #87 是計算位置光譜（build / runtime / hybrid）+ 四軸判準、本卡是 mitigation 條件光譜（baseline / stretch / trigger）+ 四 context 維度</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍是 sanity 防線</a></td>
          <td><strong>scope condition 同骨</strong> — #43 把「scope」變成顯式 fact、本卡把「deployment 條件」變成顯式 fact；都在說「不顯式 = 失控的 default 詮釋」</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>#100 的 dimension 3</strong> — context 不寫是 false sense 的第三大產地（dimension 1 = threat model 不對稱 / dimension 2 = mitigation 對位失效 / dimension 3 = context 沒寫）</td>
      </tr>
      <tr>
          <td><a href="../threat-model-explicitness/">#101 Threat model 明確性</a> + <a href="../mitigation-threat-alignment/">#102 Mitigation 對位</a></td>
          <td><strong>本卡是 #101/#102 的 condition 維度</strong> — #101 確立 in-scope threat、#102 確立 mitigation→threat 對位、本卡確立對位在 deployment 條件下的有效性；三者完整定義 mitigation 強度</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — verifiability-first 的 dimension 3</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「使用 X」單行 mitigation、沒寫 config / scale / runtime 條件</td>
          <td>補三類條件：成立 / 失效 / deployment 變數</td>
      </tr>
      <tr>
          <td>標準引用（OWASP / RFC）抄整段、沒寫適用 deployment</td>
          <td>標準是 universal-flavored、本地化 deployment context</td>
      </tr>
      <tr>
          <td>Mitigation 描述沒提 work factor / iteration count / 強度參數</td>
          <td>補強度參數 + 對應 actor 能力的 trigger condition</td>
      </tr>
      <tr>
          <td>多實例 / 多區域部署、rate limit / session 描述沒提 distributed</td>
          <td>補多實例 context、明示 local vs distributed 的差異</td>
      </tr>
      <tr>
          <td>「在 modern browser」「在 standard config」沒展開的修飾詞</td>
          <td>列舉 modern / standard 涵蓋什麼、不涵蓋什麼</td>
      </tr>
      <tr>
          <td>Threat actor 能力 / 計算成本沒列</td>
          <td>補 actor model、區分個人 / 組織 / nation-state 的 mitigation 強度</td>
      </tr>
      <tr>
          <td>「之後 deployment 不一樣再說」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 trigger</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安 mitigation 的所有論述（auth / crypto / 傳輸 / 防護 / scale-sensitive control）；任何「方法有效性受部署條件影響」的領域（concurrency primitive 在不同 memory model / DB transaction 在不同 isolation level / consensus 演算法在不同 network partition 假設）</li>
<li><strong>不適用</strong>：純歷史 / 概念介紹（不教 mitigation deployment）、研究探討（讀者預期自行 explore condition）</li>
<li><strong>邊界</strong>：「Context-dependence 顯式」≠「窮舉所有 deployment 排列組合」——只列 reader 直覺會誤判的 dimension（最常見 deployment 跟最常見變體）、不必涵蓋整個 deployment space；判別準則：「reader 用 default 條件詮釋會不會 silent 失效」——會 → 補 context、不會 → 不必補</li>
<li><strong>過度條件化反例</strong>：每個 mitigation 列 deployment matrix（10 個 dimension × 5 個值 = 50 個 case）、文章變 deployment guide、不是教學；條件描述的投資量級對應 mitigation 在系統的責任比重——核心 control（auth / crypto）值得三類條件完整、輔助 control 只列 baseline + 一個 stretch case 即可</li>
</ul>
<p>本卡是資安 audit 第三個維度（context-dependence）、配 <a href="../threat-model-explicitness/">#101</a> threat model + <a href="../mitigation-threat-alignment/">#102</a> 對位、後續 #104 citation 形成完整 audit dimension 集合。</p>
]]></content:encoded></item><item><title>Security 標準引用的時效性與精確度</title><link>https://tarrragon.github.io/blog/report/security-citation-currency-and-precision/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/security-citation-currency-and-precision/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安標準引用不是「這條 control 寫在 X 文件」、是「這條 control 在 X 版本 X 年份 X 語境下對 X actor 模型成立」。&lt;/strong> 五個變數任一變、引用就過時或扭曲。資安 best practice 衰退快、universal-flavored 引用（「OWASP 建議 X」「RFC 規定 Y」）會 silent 把過時或語境外的內容傳給 reader、產生 &lt;a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security&lt;/a> 的 citation 維度產地。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>引用形態&lt;/th>
 &lt;th>reader 套用時的判斷&lt;/th>
 &lt;th>過時 / 扭曲時的後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「OWASP 建議 X」&lt;/td>
 &lt;td>X 是 universal best practice&lt;/td>
 &lt;td>套用時 OWASP 已改版、reader 不知道&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「OWASP Top 10 (2021) 建議 X、原文：『&amp;hellip;』」&lt;/td>
 &lt;td>X 在 2021 OWASP Top 10 語境下成立&lt;/td>
 &lt;td>過時時 reader 知道要 check 新版&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差別在 reader 在實作 review 階段有沒有版本變數可檢查、有沒有原文語境可驗證。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>資安標準群（OWASP / RFC / NIST SP 800 系列 / CIS Benchmark / PCI DSS / ISO 27001）有三個跟一般技術文獻不同的特性：&lt;/p>
&lt;h3 id="特性-1best-practice-衰退速度快">特性 1：Best practice 衰退速度快&lt;/h3>
&lt;p>加密 / hashing 領域是最典型例子：&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">1995-2005：MD5 是 password hashing 常見選擇
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2005-2010：MD5 deprecated、改 SHA-1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">2010-2015：SHA-1 弱、改 bcrypt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">2015-2020：bcrypt 仍 OK、PBKDF2 100k iter 仍 OK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">2020-2026：建議升 argon2id、PBKDF2 推 600k+、bcrypt work factor 推 12+&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>任何 2020 年寫的「使用 bcrypt 即可」教學在 2026 年仍部分成立、但 work factor 推薦值已 outdated。沒標年份的引用 reader 沒有 review trigger。&lt;/p>
&lt;h3 id="特性-2原文常被引用扭曲">特性 2：原文常被引用扭曲&lt;/h3>
&lt;p>引用 chain 中常見的 drift：&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">原文（OWASP Cheat Sheet）：In contexts where session fixation is a concern, consider regenerating the session ID upon login.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">中介轉述：OWASP says regenerate session ID upon login.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">進一步引用：OWASP requires session regeneration on every login.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">最終讀者：「OWASP 強制要求每次 login 都 regenerate session」&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>語意從「conditional 建議」滑成「universal 強制」、原文的 conditional context（「session fixation is a concern」）被丟。Reader 套用時把 conditional 當 unconditional、可能在不需要的地方加複雜度、或在需要的地方因為「我已經做了」跳過 threat model 重新評估。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安標準引用不是「這條 control 寫在 X 文件」、是「這條 control 在 X 版本 X 年份 X 語境下對 X actor 模型成立」。</strong> 五個變數任一變、引用就過時或扭曲。資安 best practice 衰退快、universal-flavored 引用（「OWASP 建議 X」「RFC 規定 Y」）會 silent 把過時或語境外的內容傳給 reader、產生 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 的 citation 維度產地。</p>
<table>
  <thead>
      <tr>
          <th>引用形態</th>
          <th>reader 套用時的判斷</th>
          <th>過時 / 扭曲時的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「OWASP 建議 X」</td>
          <td>X 是 universal best practice</td>
          <td>套用時 OWASP 已改版、reader 不知道</td>
      </tr>
      <tr>
          <td>「OWASP Top 10 (2021) 建議 X、原文：『&hellip;』」</td>
          <td>X 在 2021 OWASP Top 10 語境下成立</td>
          <td>過時時 reader 知道要 check 新版</td>
      </tr>
  </tbody>
</table>
<p>差別在 reader 在實作 review 階段有沒有版本變數可檢查、有沒有原文語境可驗證。</p>
<hr>
<h2 id="情境">情境</h2>
<p>資安標準群（OWASP / RFC / NIST SP 800 系列 / CIS Benchmark / PCI DSS / ISO 27001）有三個跟一般技術文獻不同的特性：</p>
<h3 id="特性-1best-practice-衰退速度快">特性 1：Best practice 衰退速度快</h3>
<p>加密 / hashing 領域是最典型例子：</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">1995-2005：MD5 是 password hashing 常見選擇
</span></span><span class="line"><span class="ln">2</span><span class="cl">2005-2010：MD5 deprecated、改 SHA-1
</span></span><span class="line"><span class="ln">3</span><span class="cl">2010-2015：SHA-1 弱、改 bcrypt
</span></span><span class="line"><span class="ln">4</span><span class="cl">2015-2020：bcrypt 仍 OK、PBKDF2 100k iter 仍 OK
</span></span><span class="line"><span class="ln">5</span><span class="cl">2020-2026：建議升 argon2id、PBKDF2 推 600k+、bcrypt work factor 推 12+</span></span></code></pre></div><p>任何 2020 年寫的「使用 bcrypt 即可」教學在 2026 年仍部分成立、但 work factor 推薦值已 outdated。沒標年份的引用 reader 沒有 review trigger。</p>
<h3 id="特性-2原文常被引用扭曲">特性 2：原文常被引用扭曲</h3>
<p>引用 chain 中常見的 drift：</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">原文（OWASP Cheat Sheet）：In contexts where session fixation is a concern, consider regenerating the session ID upon login.
</span></span><span class="line"><span class="ln">2</span><span class="cl">中介轉述：OWASP says regenerate session ID upon login.
</span></span><span class="line"><span class="ln">3</span><span class="cl">進一步引用：OWASP requires session regeneration on every login.
</span></span><span class="line"><span class="ln">4</span><span class="cl">最終讀者：「OWASP 強制要求每次 login 都 regenerate session」</span></span></code></pre></div><p>語意從「conditional 建議」滑成「universal 強制」、原文的 conditional context（「session fixation is a concern」）被丟。Reader 套用時把 conditional 當 unconditional、可能在不需要的地方加複雜度、或在需要的地方因為「我已經做了」跳過 threat model 重新評估。</p>
<h3 id="特性-3版本之間語意可能反轉">特性 3：版本之間語意可能反轉</h3>
<p>OWASP Top 10 / NIST SP 800 系列、版本之間的 control 重點會大幅調整：</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">OWASP Top 10:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  2017 → A1 Injection / A7 XSS
</span></span><span class="line"><span class="ln">3</span><span class="cl">  2021 → A03 Injection（含 XSS、合併）/ A08 Software and Data Integrity Failures（新類別）
</span></span><span class="line"><span class="ln">4</span><span class="cl">NIST SP 800-63B:
</span></span><span class="line"><span class="ln">5</span><span class="cl">  2014 版：強制 password 定期更換
</span></span><span class="line"><span class="ln">6</span><span class="cl">  2017 版：明示**不要**強制定期更換、除非有外洩證據</span></span></code></pre></div><p>引用「NIST 建議定期更換 password」在 2014 對、2017 後是反向違反 NIST。版本不標 = reader 可能引用到反向版本。</p>
<h3 id="特性-4internal-citation-也是-citation">特性 4：Internal citation 也是 citation</h3>
<p>問題節點 / problem-node 框架的章節常用內部連結（<code>[authentication]</code> <code>[session-invalidation]</code> 等 knowledge-cards）作為「control-of-record」，把實作細節下放到子頁。這些內部連結<strong>等同 citation</strong>——指向「這個 control 由那一頁定義」、章節讀者在這層形成判斷、再決定是否點進去。</p>
<p>Internal citation 同樣有四個失效模式：</p>
<table>
  <thead>
      <tr>
          <th>失效模式</th>
          <th>外部 citation（OWASP / RFC）</th>
          <th>Internal citation（knowledge-cards / 跨章引用）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>時效衰退</td>
          <td>OWASP 改版、引用過時版本</td>
          <td>knowledge-cards 內容更新、章節引用沒同步</td>
      </tr>
      <tr>
          <td>句意 drift</td>
          <td>conditional → unconditional 轉述</td>
          <td>章節用 control-name 暗示能力、子頁定義跟暗示不一致</td>
      </tr>
      <tr>
          <td>版本反轉</td>
          <td>NIST 2014 vs 2017 password 政策反向</td>
          <td>knowledge-card rewrite、原本 in-scope 變 out-of-scope</td>
      </tr>
      <tr>
          <td>Broken / dead link</td>
          <td>URL 變更、文件下架</td>
          <td>knowledge-card 改 slug / 移檔、章節連結 silent broken</td>
      </tr>
  </tbody>
</table>
<p>外部 citation 至少有版本號當 anchor、internal citation 連版本概念都沒有——更易 silent drift。所以 audit 跟 review trigger 對 internal 反而更嚴格。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>每個 security citation 加四個欄位：</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">[Citation X]
</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">- 原文 / quote：[原文一句、不轉述]
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 引用 scope：[原文適用的 context / actor model / 前提條件]
</span></span><span class="line"><span class="ln">5</span><span class="cl">- Review trigger：[何時要 re-check 標準是否有新版]</span></span></code></pre></div><p>例（password hashing）：</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">- 標準：OWASP Password Storage Cheat Sheet（2024 update）
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 原文：「Use Argon2id with a minimum configuration of 19 MiB of memory, an iteration count of 2, and 1 degree of parallelism」
</span></span><span class="line"><span class="ln">3</span><span class="cl">- Scope：Web 應用 password hashing、針對個人 / 組織 actor、不適用 nation-state actor 或 high-throughput verification
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Review trigger：每 12 月 re-check OWASP cheat sheet 是否有新建議；GPU 算力翻倍時提前 re-check</span></span></code></pre></div><p>例（session 管理）：</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">- 標準：OWASP Session Management Cheat Sheet（2024 update）
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 原文：「Session ID should be regenerated after any privilege level change (e.g., after a successful authentication or after a session token has elevated privileges)」
</span></span><span class="line"><span class="ln">3</span><span class="cl">- Scope：Web session ID rotation、conditional 在 privilege level change 時、不是「每次 request」也不是「每次 login」（對 already-authenticated session）
</span></span><span class="line"><span class="ln">4</span><span class="cl">- Review trigger：當 application 加入新的 privilege boundary（如 admin elevation）時 re-check</span></span></code></pre></div><h3 id="引用扭曲的-audit-流程">引用扭曲的 audit 流程</h3>
<p>對章節既有引用跑驗證 pass：</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. 列出所有 citation（外部：標準 / RFC / CVE；內部：knowledge-cards 連結 / 跨章引用）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 對每條 citation 找一手來源、記錄 URL + 版本 + 年份（外部）/ 最後修改 + slug（內部）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 對比文中轉述跟原文 / 子頁定義、check 三類 drift：
</span></span><span class="line"><span class="ln">4</span><span class="cl">   - Conditional → unconditional drift（原文有條件、文中沒條件）
</span></span><span class="line"><span class="ln">5</span><span class="cl">   - Specific → general drift（原文限特定 context、文中講通用）
</span></span><span class="line"><span class="ln">6</span><span class="cl">   - Recommendation → mandate drift（原文是 consider / recommend、文中是 must / required）
</span></span><span class="line"><span class="ln">7</span><span class="cl">4. drift 找到、補回原文 conditional / scope / language strength
</span></span><span class="line"><span class="ln">8</span><span class="cl">5. 標版本跟 review trigger（外部）/ 標 last-checked + sync owner（內部）
</span></span><span class="line"><span class="ln">9</span><span class="cl">6. 內部專屬 check：連結是否 broken（slug 改了 / 檔案移了）、子頁是否仍存在 / 仍 in scope</span></span></code></pre></div><p>集合運算讓引用扭曲從「靠記憶」升級到「可驗證」。Internal citation 多兩個專屬步驟（broken link + slug drift）、跟 <a href="../url-slug-must-be-explicit-fact/">#93 URL slug 必須顯式定義為 fact</a> 同骨——identifier 跨工具 / 跨檔案沒 fact 化、就會 silent broken。</p>
<h3 id="review-trigger-的-cadence-設計">Review trigger 的 cadence 設計</h3>
<p>不同類型 citation 的 review cadence 不同：</p>
<table>
  <thead>
      <tr>
          <th>Citation 類型</th>
          <th>建議 review cadence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Crypto primitive 強度參數</td>
          <td>每 6-12 月（actor 算力會變）</td>
      </tr>
      <tr>
          <td>OWASP Top 10 / Cheat Sheet</td>
          <td>每 12-24 月（major 改版頻率）</td>
      </tr>
      <tr>
          <td>RFC（已 finalized）</td>
          <td>每 24-36 月（除非有新 RFC supersede）</td>
      </tr>
      <tr>
          <td>CVE / 特定漏洞</td>
          <td>即時（一次性事件、不需 cadence、引用後標記「fixed in vX.Y」）</td>
      </tr>
      <tr>
          <td><strong>Internal knowledge-cards</strong></td>
          <td><strong>每 6 月（內部演化快、無版本號當 anchor）</strong></td>
      </tr>
      <tr>
          <td><strong>跨章 / 跨模組引用</strong></td>
          <td><strong>每次大改子頁時 broadcast；無 broadcast 時每 6 月 sweep</strong></td>
      </tr>
      <tr>
          <td>NIST SP 800 系列</td>
          <td>每 24 月（NIST 改版頻率）</td>
      </tr>
      <tr>
          <td>PCI DSS / ISO 27001</td>
          <td>每 24-36 月（合規標準改版頻率）</td>
      </tr>
  </tbody>
</table>
<p>跟 <a href="../escalation-trigger-quantification/">#91 升級 trigger 的量化設計</a> 同骨——「之後再 review」不是 trigger、有 cadence + owner + threshold 才是 trigger。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="過時-citation-silent-變成過時實作">過時 citation silent 變成過時實作</h3>
<p>reader 信任引用、用 citation 內容實作、citation 過時後實作不知道、新 best practice 沒被採用。Crypto 領域最常見：MD5 / SHA-1 / 弱 PBKDF2 iteration / 過時 cipher suite 在生產系統留存幾十年的案例不少、原因常常是「教學 / spec 沒更新、實作跟著沒更新」。</p>
<p>跟 <a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a> 同病、citation 維度的具體展現：reader 以為「我用了標準推薦」就安全、實際標準早改、自己用的是 deprecated 版本。</p>
<h3 id="扭曲-citation-把-conditional-變強制--把-specific-變通用">扭曲 citation 把 conditional 變強制 / 把 specific 變通用</h3>
<p>引用扭曲的後果有兩面：</p>
<ul>
<li><strong>Conditional → unconditional</strong>：reader 在不需要的地方加複雜度、團隊成本上升、卻不解決真 threat</li>
<li><strong>Specific → general</strong>：reader 把特定 context 的 control 套到不同 context、可能 silent 失效</li>
</ul>
<p>兩面都讓 mitigation 跟 threat 對位錯誤（<a href="../mitigation-threat-alignment/">#102 mitigation-threat-alignment</a>）。</p>
<h3 id="引用-chain-越長扭曲累積越嚴重">引用 chain 越長、扭曲累積越嚴重</h3>
<p>教學 → 教學 → 教學 的 chain 中、每一層轉述都可能 drift。citation 沒回到一手原文、整條 chain 共享同一個扭曲、攻擊者繞過扭曲版的 mitigation 一次、所有採用該 chain 的 implementation 都中。<strong>citation 的時效跟精確不是個別文章問題、是 ecosystem 問題</strong>——一手原文 + 版本 + 原文 quote 是 minimum cost 的修法。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review</a></td>
          <td><strong>citation 是 metadata surface 的延伸</strong> — citation 是讀者的「外部 source」入口、跟 title / heading / link label 並列為 metadata；本卡是 #97 在引用維度的展開</td>
      </tr>
      <tr>
          <td><a href="../url-slug-must-be-explicit-fact/">#93 URL slug 必須顯式定義為 fact</a></td>
          <td><strong>identifier 同骨 + internal citation 強相關</strong> — slug 是內部 identifier、外部 citation / 內部 citation 都需要 explicit fact（版本 / 年份 / 原文 / slug + last-checked）；internal citation 沒版本號當 anchor、跟 #93 SSoT 違反同類風險、broken-link / drift 是 internal citation 專屬失效</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td><strong>同骨 ceiling</strong> — 引用標準名稱 = 字面、引用句意對到原文 context = 行為；stop at 字面 = false confidence</td>
      </tr>
      <tr>
          <td><a href="../escalation-trigger-quantification/">#91 升級 trigger 的量化設計</a></td>
          <td><strong>review trigger 同骨</strong> — #91 在 capability 升級的 trigger 設計、本卡在 citation review 的 cadence 設計；都是「沒 trigger = 結構性跳過」</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>#100 的 dimension 4</strong> — citation 過時 / 扭曲是 false sense 的第四大產地（dimension 1-3 = threat / mitigation / context、本卡 = 引用 source）</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — verifiability-first 的 dimension 4</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用「OWASP / NIST / RFC / CIS」沒標年份 / 版本</td>
          <td>補版本 + 年份、確認當前是否仍是 current</td>
      </tr>
      <tr>
          <td>引用是轉述、沒原文 quote</td>
          <td>找一手來源、補原文 quote、check 是否被 drift</td>
      </tr>
      <tr>
          <td>「OWASP <strong>建議</strong> X」「RFC <strong>規定</strong> Y」當 universal</td>
          <td>補 scope（在什麼 context / actor model 下成立）</td>
      </tr>
      <tr>
          <td>Crypto / hashing 強度參數是固定值（10 / 100k / 32 char）</td>
          <td>補 review trigger（每 6-12 月 re-check actor 算力跟標準）</td>
      </tr>
      <tr>
          <td>Citation 是「最佳實踐」「業界標準」當 anchor、沒列具體文件</td>
          <td>補具體標準名稱 + 版本、不能用 vague reference</td>
      </tr>
      <tr>
          <td>章節寫於 N 年前、沒提 last reviewed 日期</td>
          <td>補 last reviewed 標記、設下次 review trigger</td>
      </tr>
      <tr>
          <td>Conditional 原文被引成 unconditional（「強制」「必須」「總是」）</td>
          <td>找原文 conditional context、補回 scope qualifier</td>
      </tr>
      <tr>
          <td>「之後標準改了再更新」</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 review cadence + owner</td>
      </tr>
      <tr>
          <td>章節用 internal link（knowledge-cards / 跨章引用）作為 control-of-record、沒 last-checked / sync owner</td>
          <td>等同未驗證的 citation；補 last-checked + sync owner、子頁大改時 broadcast 到引用方</td>
      </tr>
      <tr>
          <td>Internal link 連結還在但目標頁 slug / 內容已改、章節原本暗示的 control 跟現在不對應</td>
          <td>Silent broken / drift；定期跑連結 sweep + 文意對比、跟外部 citation 同流程處理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容引用標準（auth / crypto / 傳輸 / 防護 / 合規）；<strong>內部 citation</strong>（knowledge-cards 連結、跨章 / 跨模組引用作為 control-of-record）；任何 best practice 衰退快、版本之間語意會反轉的領域（cloud security 配置、container 安全、特定 framework 的安全 idiom）</li>
<li><strong>不適用</strong>：純歷史 / 概念介紹（不依賴 current best practice）、學術 retrospective（討論 historical 標準時版本本身是內容）</li>
<li><strong>邊界</strong>：「citation 時效跟精確」≠「窮舉所有版本變更」——只列當前文章涵蓋 scope 的 citation、追到一手 + 版本 + scope qualifier 即可；判別準則：「如果這條 citation 過時或語境變、reader 會做錯什麼？」——會做錯 → 補完整四欄位；不會做錯（純歷史 reference）→ 標年份即可</li>
<li><strong>過度引用反例</strong>：每段話都附 citation 鏈 + 原文 quote + 三條 review trigger、文章變 footnote-driven、reader 讀不下去；citation 投資量級對應該段對 reader 實作的影響——核心 mitigation 段值得四欄位完整、background 段標版本 + URL 即可</li>
</ul>
<p>本卡是資安 audit 第四個維度（citation）、配 <a href="../threat-model-explicitness/">#101</a> / <a href="../mitigation-threat-alignment/">#102</a> / <a href="../mitigation-context-dependence/">#103</a> 形成完整 audit dimension 集合（threat / mitigation / context / citation）。後續 <a href="../security-audit-recommendation-tiers/">#105 audit recommendation 層級</a> 把四維度的 weakness 統合成 recommendation 決策。</p>
]]></content:encoded></item><item><title>Audit recommendation 層級：accept / minor / major / 教錯不可保留</title><link>https://tarrragon.github.io/blog/report/security-audit-recommendation-tiers/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/security-audit-recommendation-tiers/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>資安 audit 的 recommendation 是 ship 決策、不是評語。&lt;/strong> 把每個 weakness trace 到具體 tier、輸出可被 build process / publish gate 引用——不該停在「這裡可改善」的軟性建議。四個 tier 是 monotonic decision shape：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tier&lt;/th>
 &lt;th>意涵&lt;/th>
 &lt;th>Ship 決策&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Accept&lt;/td>
 &lt;td>無 weakness 或全在容忍範圍&lt;/td>
 &lt;td>直接 ship&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Minor revise&lt;/td>
 &lt;td>邊界 / contrast / 版本標記類小改&lt;/td>
 &lt;td>補完即可 ship、不阻擋 timeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Major revise&lt;/td>
 &lt;td>結構性 false sense / 對位失效&lt;/td>
 &lt;td>重寫對應段、ship 前必須修復&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Withdraw&lt;/strong>&lt;/td>
 &lt;td>內容主動誤導、ship = 增加 risk&lt;/td>
 &lt;td>&lt;strong>必須移除或全換、不存在 ship&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第四層是資安 audit 跟一般學術 peer review 的關鍵差異——學術 reject 會給投稿者改寫機會、本 audit 的 withdraw 是「&lt;strong>保留 = 增加生產系統 risk&lt;/strong>」的硬決策。跟 &lt;a href="../incremental-shipping-criteria/">#76 incremental shipping criteria&lt;/a> 反向：可逆內容可分批 ship 改善、不可逆 risk 內容不能。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>audit 報告若只給「找到 N 個問題」的 flat list、團隊收到後無法決策、最後常變成「慢慢改」、article ship 跟 audit 改善的 timeline 完全脫鉤。Tier 化的 recommendation 把 weakness 轉成決策訊號：&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">Flat list（沒層級）：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">- 第 3 段沒寫 threat model boundary
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">- 第 5 段 mitigation 沒寫 mechanism
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">- 第 7 段引用 OWASP 沒標版本
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">- 第 9 段 bcrypt work factor = 10、針對 nation-state 弱
&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;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">Tiered（分層）：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">- Withdraw: 第 9 段 bcrypt work factor 描述會直接讓 reader 用 weak setting、必須改寫或移除
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">- Major revise: 第 5 段 defense theater、整段重寫 mechanism + 前提
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">- Minor revise: 第 3 段補 threat model 對稱、第 7 段補 OWASP 版本
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">決策結果：第 9 段必須現在改、第 5 段下個 sprint 改、第 3/7 段順手補&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>層級給的是「&lt;strong>先做什麼 / 什麼擋 ship / 什麼可緩&lt;/strong>」的明確排序、不是改善優先序的軟建議。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="四-tier-判準">四 tier 判準&lt;/h3>
&lt;p>每個 weakness 套這個決策樹：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>資安 audit 的 recommendation 是 ship 決策、不是評語。</strong> 把每個 weakness trace 到具體 tier、輸出可被 build process / publish gate 引用——不該停在「這裡可改善」的軟性建議。四個 tier 是 monotonic decision shape：</p>
<table>
  <thead>
      <tr>
          <th>Tier</th>
          <th>意涵</th>
          <th>Ship 決策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Accept</td>
          <td>無 weakness 或全在容忍範圍</td>
          <td>直接 ship</td>
      </tr>
      <tr>
          <td>Minor revise</td>
          <td>邊界 / contrast / 版本標記類小改</td>
          <td>補完即可 ship、不阻擋 timeline</td>
      </tr>
      <tr>
          <td>Major revise</td>
          <td>結構性 false sense / 對位失效</td>
          <td>重寫對應段、ship 前必須修復</td>
      </tr>
      <tr>
          <td><strong>Withdraw</strong></td>
          <td>內容主動誤導、ship = 增加 risk</td>
          <td><strong>必須移除或全換、不存在 ship</strong></td>
      </tr>
  </tbody>
</table>
<p>第四層是資安 audit 跟一般學術 peer review 的關鍵差異——學術 reject 會給投稿者改寫機會、本 audit 的 withdraw 是「<strong>保留 = 增加生產系統 risk</strong>」的硬決策。跟 <a href="../incremental-shipping-criteria/">#76 incremental shipping criteria</a> 反向：可逆內容可分批 ship 改善、不可逆 risk 內容不能。</p>
<hr>
<h2 id="情境">情境</h2>
<p>audit 報告若只給「找到 N 個問題」的 flat list、團隊收到後無法決策、最後常變成「慢慢改」、article ship 跟 audit 改善的 timeline 完全脫鉤。Tier 化的 recommendation 把 weakness 轉成決策訊號：</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">Flat list（沒層級）：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">- 第 3 段沒寫 threat model boundary
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">- 第 5 段 mitigation 沒寫 mechanism
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">- 第 7 段引用 OWASP 沒標版本
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">- 第 9 段 bcrypt work factor = 10、針對 nation-state 弱
</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><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">Tiered（分層）：
</span></span><span class="line"><span class="ln">10</span><span class="cl">- Withdraw: 第 9 段 bcrypt work factor 描述會直接讓 reader 用 weak setting、必須改寫或移除
</span></span><span class="line"><span class="ln">11</span><span class="cl">- Major revise: 第 5 段 defense theater、整段重寫 mechanism + 前提
</span></span><span class="line"><span class="ln">12</span><span class="cl">- Minor revise: 第 3 段補 threat model 對稱、第 7 段補 OWASP 版本
</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">決策結果：第 9 段必須現在改、第 5 段下個 sprint 改、第 3/7 段順手補</span></span></code></pre></div><p>層級給的是「<strong>先做什麼 / 什麼擋 ship / 什麼可緩</strong>」的明確排序、不是改善優先序的軟建議。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="四-tier-判準">四 tier 判準</h3>
<p>每個 weakness 套這個決策樹：</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">Q1：reader 照這段實作會不會主動產生破口？
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  是 → Withdraw（不可保留）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  否 → Q2
</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">Q2：weakness 是結構性（多 dimension 同時失效）還是局部（單一 dimension 缺）？
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  結構性 → Major revise
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  局部 → Q3
</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">Q3：補完 weakness 的 cost 是「補一句 / 一表」還是「重寫一段」？
</span></span><span class="line"><span class="ln">10</span><span class="cl">  一句 / 一表 → Minor revise
</span></span><span class="line"><span class="ln">11</span><span class="cl">  重寫一段 → Major revise
</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">Q4：weakness 在容忍範圍（背景段 / 低 stakes 段、reader 不會直接照做）？
</span></span><span class="line"><span class="ln">14</span><span class="cl">  在 → Accept（可選 minor 但不要求）
</span></span><span class="line"><span class="ln">15</span><span class="cl">  不在 → 走 Q3</span></span></code></pre></div><h3 id="各-tier-的-fix-模式">各 tier 的 fix 模式</h3>
<table>
  <thead>
      <tr>
          <th>Tier</th>
          <th>Fix 模式</th>
          <th>Ship gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Accept</td>
          <td>無 fix 或自願性 minor</td>
          <td>不阻擋</td>
      </tr>
      <tr>
          <td>Minor revise</td>
          <td>補 boundary / 加 contrast / 標版本 / 補連結</td>
          <td>不阻擋（可 follow-up）</td>
      </tr>
      <tr>
          <td>Major revise</td>
          <td>重寫段落 + 補 mechanism / 前提 / context</td>
          <td>阻擋直到 fix 完成</td>
      </tr>
      <tr>
          <td>Withdraw</td>
          <td>移除整段 / 加 deprecation banner + redirect / 全換現代版</td>
          <td>阻擋直到處理</td>
      </tr>
  </tbody>
</table>
<h3 id="withdraw-的具體訊號">Withdraw 的具體訊號</h3>
<p>什麼狀態算 withdraw？四個訊號：</p>
<ol>
<li><strong>過時 crypto / hashing primitive 沒 deprecation 標記</strong>：教 MD5 / SHA-1 / 弱 PBKDF2 但沒明示「這是過時、不要用」</li>
<li><strong>扭曲 citation 改變原文語意</strong>：把 OWASP conditional 引成 unconditional、或反向違反現行標準（NIST 的 password 定期更換 case）</li>
<li><strong>違反 current best practice 的步驟說明</strong>：教讀者主動關閉 mitigation（disable HSTS / CSP / SameSite）作為 workaround、沒明示「workaround 引入的新 risk」</li>
<li><strong>Defense theater 例子當示範</strong>：用名稱層 mitigation 對位（rate limit「擋」brute force）作為步驟、reader 照做不擋實際 mechanism</li>
</ol>
<p>四訊號的共通：<strong>reader 照做後實作會主動 worse than not having read</strong>。Withdraw 不是嚴格、是 risk-asymmetric（<a href="../security-teaching-rigor-asymmetry/">#99</a>）下的必要決策。</p>
<h3 id="audit-report-輸出格式">Audit report 輸出格式</h3>
<p>學術 peer review 的格式對應到本 audit：</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"># Audit Report: &lt;章節 / 文章 title&gt;
</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">## Summary
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">&lt;1-2 句：主要 audit 結論 + 整體 tier&gt;
</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">## Strengths
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">- &lt;段 / dimension 跟其優點&gt;
</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">## Weaknesses by dimension
</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">### Threat model（[#101](../threat-model-explicitness/)）
</span></span><span class="line"><span class="ln">12</span><span class="cl">- [Tier]: 段 N、[具體 weakness 描述]、[fix 建議]
</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">### Mitigation 對位（[#102](../mitigation-threat-alignment/)）
</span></span><span class="line"><span class="ln">15</span><span class="cl">- ...
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">### Context-dependence（[#103](../mitigation-context-dependence/)）
</span></span><span class="line"><span class="ln">18</span><span class="cl">- ...
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">### Citation（[#104](../security-citation-currency-and-precision/)）
</span></span><span class="line"><span class="ln">21</span><span class="cl">- ...
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">## Blocking conditions
</span></span><span class="line"><span class="ln">24</span><span class="cl">&lt;必須 fix 才能 ship 的 weakness 清單、按 tier 排序&gt;
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl">## Recommendation
</span></span><span class="line"><span class="ln">27</span><span class="cl">&lt;Accept / Minor revise / Major revise / Withdraw + 整體決策說明&gt;</span></span></code></pre></div><p>格式跟學術 peer review 同骨、欄位對應 audit dimension（#101-104）、輸出可直接餵 ship gate 工具。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="audit-變評語改善-timeline-跟-ship-完全脫鉤">Audit 變評語、改善 timeline 跟 ship 完全脫鉤</h3>
<p>flat list 的 audit 給「找到問題」、team 把問題列入 backlog、backlog 永遠排不到上面（<a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發會被結構性跳過</a>）。tier 化讓 audit 從「評語」變「ship 決策 input」、跟 timeline 強耦合。</p>
<h3 id="withdraw-level-內容繼續-ship生產系統-risk-持續累積">Withdraw-level 內容繼續 ship、生產系統 risk 持續累積</h3>
<p>最危險的 case 是 audit 找到 withdraw-level weakness（過時 crypto、扭曲 citation）但用 minor / major 處置——讓內容繼續存在並擴散。教學擴散 = silent gap 集體放大（<a href="../false-sense-of-security-as-primary-failure/">#100 false sense of security</a>），withdraw 是 cut-off 訊號、不是嚴格、是必要。</p>
<h3 id="各-tier-之間的決策邏輯模糊reviewer-之間判準不一致">各 tier 之間的決策邏輯模糊、reviewer 之間判準不一致</h3>
<p>沒明確 tier 判準、不同 reviewer 對同一個 weakness 給不同建議——有人覺得「補一行就好」（minor）、有人覺得「整段重寫」（major）、有人覺得「移除」（withdraw）。決策不一致 = audit 失去結構性 value、退化成個人意見集合。tier 判準（決策樹四問題）讓判準可重現、跨 reviewer 收斂。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../decision-presentation-options-recommendation/">#74 決策呈現：選項 + 推薦 + 開放修改</a></td>
          <td><strong>同骨決策呈現</strong> — #74 是給 user 決策的 options + recommendation 模板、本卡是給 ship gate 的 tier + recommendation 模板；都把整理成本攤開、不丟「你想怎麼做」開放問</td>
      </tr>
      <tr>
          <td><a href="../incremental-shipping-criteria/">#76 分批 ship：低風險可見價值先行</a></td>
          <td><strong>反面對照</strong> — #76 適用可逆內容、本卡的 withdraw 適用不可逆 risk 內容、分批 ship 邏輯不適用；本卡是 #76 在 risk-asymmetric 領域的硬邊界</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度</a></td>
          <td><strong>本卡的決策維度</strong> — #79 是 meta、本卡是其中「呈現 + 策略疊加 + 批次」三維在 audit 報告的具體實現</td>
      </tr>
      <tr>
          <td><a href="../escalation-trigger-quantification/">#91 升級 trigger 的量化設計</a></td>
          <td><strong>withdraw 是 blocking trigger</strong> — #91 在 capability 升級的 trigger 設計、本卡的 withdraw 是 ship 阻擋的 trigger；都是「沒明確 trigger = 不會 fire」</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td><strong>本卡是消滅 #100 的 ship 決策面</strong> — #101-104 是發現 false sense 的維度、本卡是發現後的處置決策</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 資安教學審查標準對應風險不對稱</a></td>
          <td>上游動機 — risk-asymmetric 直接驅動 withdraw tier 的存在；一般 audit（一般教學）只需要 accept / minor / major、資安 audit 必須加 withdraw</td>
      </tr>
      <tr>
          <td><a href="../yes-no-binary-collapse/">#80 Yes/No 二選 collapse</a></td>
          <td><strong>避免 collapse</strong> — 「audit 通過嗎」是 yes/no collapse、tier 化是把 1 bit 展開成 4 個 monotonic 層級、保留決策維度</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Audit 結論是「找到 N 個問題」flat list</td>
          <td>把每個 weakness 跑 tier 決策樹、輸出 tier-grouped report</td>
      </tr>
      <tr>
          <td>找到過時 crypto / 扭曲 citation 但給 minor revise</td>
          <td>升級到 withdraw、ship gate 必須阻擋</td>
      </tr>
      <tr>
          <td>「之後改善」「下個版本補」當 weakness 處置</td>
          <td>是 <a href="../external-trigger-for-high-roi-work/">#72</a> 結構性跳過、補 ship gate 強制 trigger</td>
      </tr>
      <tr>
          <td>不同 reviewer 對同 weakness 給不同 tier</td>
          <td>補決策樹、跑判準收斂</td>
      </tr>
      <tr>
          <td>Audit pass 但實作後事故、回溯到 audit 沒 catch 的 weakness</td>
          <td>補 weakness 到對應 dimension（#101-104）、檢查 tier 判準是否需調整</td>
      </tr>
      <tr>
          <td>沒「strengths」段</td>
          <td>補 strengths、reviewer 視角不只 weakness、strengths 是 audit completeness 的訊號</td>
      </tr>
      <tr>
          <td>Recommendation 沒明確 ship gate 對應</td>
          <td>補 blocking conditions 段、明示哪些 tier 阻擋 ship</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：資安內容 audit 的產出格式（章節 audit / 文章 audit / 跨章節 review）；任何「reader 照做後錯誤不可逆」的高 stakes 領域 audit（concurrency 正確性、distributed consistency、financial / medical 計算）</li>
<li><strong>不適用</strong>：一般技術內容 audit（不需要 withdraw tier、accept / minor / major 三層即可）、研究探討文章的 review（學術 reject 跟 withdraw 語意不同）</li>
<li><strong>邊界</strong>：「Withdraw」≠「全文重寫」——可以是「移除有問題的段 + 加 deprecation 標 + redirect 到 current best practice 段」、不必整篇重做；判別準則：「reader 看到這個處置版本後、會不會用過時 / 扭曲版本實作？」——不會 → withdraw 處置 OK、會 → 需要更深的處置（移除整段 / 整篇）</li>
<li><strong>過度 tier 化反例</strong>：把每個段都評 tier、文章變評分表、reviewer 投資爆炸；tier 投資量級對應內容對 reader 實作的影響——核心 mitigation 段需 tier、background 段直接 accept 即可</li>
</ul>
<p>本卡是資安 audit 系列（#99-105）的決策面收尾、把 #101-104 四個 dimension 的 weakness 統合成 ship 決策。後續對應的 skill reference（<code>auditing-articles.md</code>）會以本卡的 tier + report 格式為輸出模板。</p>
]]></content:encoded></item><item><title>用 Next-action frame 取代 Disclaimer：把 prohibition 翻成 actionable chain</title><link>https://tarrragon.github.io/blog/report/next-action-frame-over-disclaimer/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/next-action-frame-over-disclaimer/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>Audit / risk 識別 reader 不該做 X 後、寫作回應的 frame 決定整段自然 positive 還是 disclaimer。&lt;/strong> 兩個 frame：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Frame&lt;/th>
 &lt;th>段落主體&lt;/th>
 &lt;th>Reader 拿到&lt;/th>
 &lt;th>自然句法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Disclaimer frame&lt;/td>
 &lt;td>告訴 reader「不要 X / X is dangerous」&lt;/td>
 &lt;td>Prohibition + warning&lt;/td>
 &lt;td>Negative phrasing&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Next-action frame&lt;/td>
 &lt;td>告訴 reader「沿 chain 做 Y / Z」&lt;/td>
 &lt;td>Chain 步驟 + 完成判準&lt;/td>
 &lt;td>Positive phrasing&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩 frame 對應同一個 risk、但 reader 拿到的東西不同。Disclaimer frame 自然產出負面陳述；逐句翻成正向句法後 frame 仍是 disclaimer、後續 multi-pass review 會繼續 catch 到負面殘餘。&lt;strong>整段 reframe 成 next-action chain 才是根治、不是字面換句。&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>實際 case：對資安 problem-node 章節跑 audit、找到 D2 Major weakness——「reader 拿章節層 control name 直接 ship、會產生 false sense of security」。寫作回應的選擇：&lt;/p>
&lt;p>&lt;strong>v1（Disclaimer frame，直覺選擇）&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>章節給的是 routing layer、不是 implementation layer。判讀完成 ≠ 控制面實作完成。Reader 拿章節層 control 名稱直接 ship、會產生 false sense of security。&lt;/p>&lt;/blockquote>
&lt;p>「不是」「≠」「會產生」一連串負面陳述。Multi-pass review catch 到、要求正向改寫。&lt;/p>
&lt;p>&lt;strong>v2（Disclaimer frame + 字面正向化）&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>判讀完成代表 routing 階段交付；實作完成要靠 mechanism 層跟下游模組接續。Reader 拿章節層 control 名稱直接 ship、會建立 false sense of security——routing 跟 implementation 是兩個分開的交付里程碑。&lt;/p>&lt;/blockquote>
&lt;p>字面去掉「不是」「≠」、但 frame 仍是 disclaimer——主軸是「警告 reader 別把 routing 當 implementation」、reader 拿到的仍是 prohibition + warning、actionable next step 隱含在「靠下游接續」一句。&lt;/p>
&lt;p>&lt;strong>v3（Next-action frame，reframe 後）&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>本章交付三樣：問題節點清單、判讀訊號、控制面 link。判讀完成後沿兩條 chain 進入 implementation：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Mechanism chain&lt;/strong>：點問題節點表的 &lt;code>[control-name]&lt;/code> link 進 knowledge-card、那層展開機制 / 邊界 / context-dependence。&lt;/li>
&lt;li>&lt;strong>Delivery chain&lt;/strong>：「交接路由」欄位指向下游模組（05 / 06 / 08 視章節而定）、接配置 / 驗證 / 處置交付。&lt;/li>
&lt;/ol>
&lt;p>Implementation 強度取決於兩條 chain 的完成度。&lt;/p>&lt;/blockquote>
&lt;p>整段 positive、不需 contrast。Reader 拿到兩條 actionable chain + 完成判準。Risk 仍被處理（chain 不走完 = implementation 沒交付）、用「該做什麼」取代「不要做什麼」。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>寫 audit response 段前先問 frame：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>Audit / risk 識別 reader 不該做 X 後、寫作回應的 frame 決定整段自然 positive 還是 disclaimer。</strong> 兩個 frame：</p>
<table>
  <thead>
      <tr>
          <th>Frame</th>
          <th>段落主體</th>
          <th>Reader 拿到</th>
          <th>自然句法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Disclaimer frame</td>
          <td>告訴 reader「不要 X / X is dangerous」</td>
          <td>Prohibition + warning</td>
          <td>Negative phrasing</td>
      </tr>
      <tr>
          <td>Next-action frame</td>
          <td>告訴 reader「沿 chain 做 Y / Z」</td>
          <td>Chain 步驟 + 完成判準</td>
          <td>Positive phrasing</td>
      </tr>
  </tbody>
</table>
<p>兩 frame 對應同一個 risk、但 reader 拿到的東西不同。Disclaimer frame 自然產出負面陳述；逐句翻成正向句法後 frame 仍是 disclaimer、後續 multi-pass review 會繼續 catch 到負面殘餘。<strong>整段 reframe 成 next-action chain 才是根治、不是字面換句。</strong></p>
<hr>
<h2 id="情境">情境</h2>
<p>實際 case：對資安 problem-node 章節跑 audit、找到 D2 Major weakness——「reader 拿章節層 control name 直接 ship、會產生 false sense of security」。寫作回應的選擇：</p>
<p><strong>v1（Disclaimer frame，直覺選擇）</strong>：</p>
<blockquote>
<p>章節給的是 routing layer、不是 implementation layer。判讀完成 ≠ 控制面實作完成。Reader 拿章節層 control 名稱直接 ship、會產生 false sense of security。</p></blockquote>
<p>「不是」「≠」「會產生」一連串負面陳述。Multi-pass review catch 到、要求正向改寫。</p>
<p><strong>v2（Disclaimer frame + 字面正向化）</strong>：</p>
<blockquote>
<p>判讀完成代表 routing 階段交付；實作完成要靠 mechanism 層跟下游模組接續。Reader 拿章節層 control 名稱直接 ship、會建立 false sense of security——routing 跟 implementation 是兩個分開的交付里程碑。</p></blockquote>
<p>字面去掉「不是」「≠」、但 frame 仍是 disclaimer——主軸是「警告 reader 別把 routing 當 implementation」、reader 拿到的仍是 prohibition + warning、actionable next step 隱含在「靠下游接續」一句。</p>
<p><strong>v3（Next-action frame，reframe 後）</strong>：</p>
<blockquote>
<p>本章交付三樣：問題節點清單、判讀訊號、控制面 link。判讀完成後沿兩條 chain 進入 implementation：</p>
<ol>
<li><strong>Mechanism chain</strong>：點問題節點表的 <code>[control-name]</code> link 進 knowledge-card、那層展開機制 / 邊界 / context-dependence。</li>
<li><strong>Delivery chain</strong>：「交接路由」欄位指向下游模組（05 / 06 / 08 視章節而定）、接配置 / 驗證 / 處置交付。</li>
</ol>
<p>Implementation 強度取決於兩條 chain 的完成度。</p></blockquote>
<p>整段 positive、不需 contrast。Reader 拿到兩條 actionable chain + 完成判準。Risk 仍被處理（chain 不走完 = implementation 沒交付）、用「該做什麼」取代「不要做什麼」。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>寫 audit response 段前先問 frame：</p>
<h3 id="frame-選擇判準">Frame 選擇判準</h3>
<h4 id="1-本段給-reader-什麼">1. 本段給 reader 什麼</h4>
<ul>
<li>「不要做 X」→ disclaimer frame（避開）</li>
<li>「做 Y / Z」→ next-action frame（採用）</li>
</ul>
<h4 id="2-能否把不要-x翻成該做-y--z">2. 能否把「不要 X」翻成「該做 Y / Z」</h4>
<ul>
<li>多數 case 可以、因為 risk 通常對應 missing action</li>
<li>例：「不要把 routing 當 implementation」= 「沿 mechanism + delivery 兩條 chain 走完」</li>
<li>例：「不要用 universal mitigation」= 「對稱寫 in-scope + out-of-scope + 補強路由」（<a href="../threat-model-explicitness/">#101</a>）</li>
<li>例：「不要用名稱層 mitigation 對位」= 「補 mechanism + 前提兩層」（<a href="../mitigation-threat-alignment/">#102</a>）</li>
</ul>
<h4 id="3-段落主體寫-y--z-chaincompleteness-判準明示">3. 段落主體寫 Y / Z chain、completeness 判準明示</h4>
<ul>
<li>Chain 步驟具體可執行</li>
<li>完成判準明示（reader 知道何時 chain 走完）</li>
</ul>
<h4 id="4-若必須提到-risk放段落結尾subordinate-結構">4. 若必須提到 risk、放段落結尾、subordinate 結構</h4>
<ul>
<li>Risk 是 chain 不走完的後果、放段尾一行</li>
<li>Main flow 仍是 chain、risk 是 chain 失敗的描述</li>
<li>例：「兩條 chain 走完，控制面交付完整。Chain 走不完，章節閱讀只完成 routing 階段。」</li>
</ul>
<h3 id="跟-94-的分工">跟 #94 的分工</h3>
<p><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a> 處理「保留 contrast 怎麼正向」——適用於 contrast 是論述完整性必需的 case（X、不是 Y 是讀者直覺替代）。本卡處理「整段能否不需 contrast」——適用於 disclaimer frame 整段。</p>
<p>兩卡互補：</p>
<ul>
<li>先跑本卡：能否 reframe 成 next-action？能 → 不需 contrast、整段自然 positive</li>
<li>再跑 #94：必須保留 contrast 的 case（reader 直覺替代是 Y、明確排除）→ 用「X、不是 Y」+ reasoning 結構</li>
</ul>
<p>順序錯（先跑 #94 再考慮 reframe）= 在 disclaimer frame 內逐句正向化、frame 沒動、surface fix。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="disclaimer-段反覆陷入字面-surface-fix">Disclaimer 段反覆陷入字面 surface fix</h3>
<p>整段 frame 是「不要 X」、逐句改 positive 後仍是「不要 X」變體。Multi-pass review 會反覆 catch 到負面殘餘、每次只改字面、frame 沒動。跟 <a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a> 同骨——字面層補丁不解行為層問題。</p>
<h3 id="reader-拿到-prohibition不知道-actionable-next-step">Reader 拿到 prohibition、不知道 actionable next step</h3>
<p>Disclaimer 給 reader 的 actionable 是「不要做」、但「該做什麼」reader 自己腦補。多 reader 各自腦補不同 next step、結果各不相同。Next-action frame 把 next step 寫進文字、跨 reader 一致。</p>
<h3 id="self-case本系列實際遇到">Self-case：本系列實際遇到</h3>
<p>寫資安章節「從本章到實作」段時：</p>
<ol>
<li>第一次 batch（v1，8 章）：disclaimer frame、negative phrasing</li>
<li>Multi-pass review catch、第二次 batch（v2，8 章 sed 換句）：字面去否定詞、frame 仍 disclaimer</li>
<li>User 指出「應該重新思考為什麼這樣寫」：浮現 frame 議題、reframe v3</li>
</ol>
<p>兩次 batch 改 8 章是高 cost surface fix；frame 反思在 v1 之前就該做、避開兩次無效 batch。<strong>Disclaimer 段一出現、第一反應該是「能不能 reframe」、不是「怎麼把否定詞翻正向」。</strong></p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫保留對照論據</a></td>
          <td><strong>本卡是 #94 的上游</strong> — #94 處理「contrast 是論述完整性必需時怎麼正向」、本卡處理「整段能否不需 contrast」；frame 選對、#94 議題自然消失</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>Disclaimer 是寫作便利（從 audit 警告語言抄）、跟 reader 意圖（actionable next step）反向；本卡是 #67 在 audit response 維度的展現</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>把「不是」改「兩個分開」是字面 fix、reframe 整段是行為層 fix；本卡是 #82 在寫作 frame 的具體實例</td>
      </tr>
      <tr>
          <td><a href="../false-sense-of-security-as-primary-failure/">#100 False sense of security 主要失敗模式</a></td>
          <td>Audit 識別 reader 風險、寫作端該翻成 actionable next step、不該照搬 audit 警告語言當 disclaimer</td>
      </tr>
      <tr>
          <td><a href="../transformation-at-outer-layer-when-engine-closed/">#88 Engine 不可調時把 transformation 移到外層</a></td>
          <td>同骨 transformation 邏輯——prohibition 改 next-action 是把 transformation 移到 reader 動作層、不停在警告層</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass scope 要蓋同類風險區</a></td>
          <td>Disclaimer frame 在多章重複時、batch surface fix 會在每章踩同樣 frame issue；reframe 要蓋整個 scope corpus、不只首章</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td>本卡補 multi-pass review 的 frame 軸——輪 3 機會成本語氣只 catch 字面絕對詞、frame 議題要在生成階段就反思、不能完全靠後輪 review</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段落出現「不是」「≠」「不要」「不可以」連串</td>
          <td>字面換句之前先檢查段落 frame 是否 disclaimer</td>
      </tr>
      <tr>
          <td>整段在告訴 reader「X 是 dangerous / 會產生 Y」</td>
          <td>Reframe：把 X 對應的 missing action 寫進段落主體</td>
      </tr>
      <tr>
          <td>字面翻成 positive 後、段落仍像警告</td>
          <td>Frame 沒換、reframe 整段為 next-action chain</td>
      </tr>
      <tr>
          <td>Reader 讀完不知道下一步該做什麼</td>
          <td>Disclaimer 沒給 actionable next step、補 chain</td>
      </tr>
      <tr>
          <td>同模板段在多章重複出現、每章都是負面 frame</td>
          <td>Frame 議題系統性、不是個案；改 frame + 考慮 SSoT 化（單一定義 + 各章引用）</td>
      </tr>
      <tr>
          <td>Multi-pass review 反覆 catch 同段「絕對詞太多」</td>
          <td>字面 fix 不解、reframe 段落</td>
      </tr>
      <tr>
          <td>Audit findings 直接抄進章節當警告</td>
          <td>Audit 語言是 reviewer 視角、章節 reader 需要 actionable chain</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：寫 audit / risk 的回應段、prohibition 段、warning 段、disclaimer 段；任何起手是「告訴 reader 不要做 X」的段落</li>
<li><strong>不適用</strong>：純 reference 段（不傳達 risk、純列 fact）、合規必填 disclaimer（法律 / 合約必須照規定文字）、安全告警（值班場景需要強烈警示語、reader 已是 expert）</li>
<li><strong>邊界</strong>：「Reframe 成 next-action」≠「刪掉所有警示」——risk 嚴重 / actionable 不明時、保留 warning subordinate 在段落結尾、main flow 仍是 next-action chain；判別準則：「reader 讀完段落、能否列出 next step？」能 → frame OK、不能 → 仍是 disclaimer</li>
<li><strong>過度應用反例</strong>：所有段都 reframe 成 next-action、把 reference / 規格段也改成「該做 Y」、reader 找不到 fact reference；frame 選擇對應段落責任、不是普世 rule</li>
</ul>
<hr>
<h2 id="對-multi-pass-review-的補丁">對 multi-pass review 的補丁</h2>
<p><a href="../writing-multi-pass-review/">#83 multi-pass review</a> 的輪 3「機會成本語氣」目前只跑 grep 抓絕對詞（應該 / 必須 / 不行 / 不可以 / 正確）、catch 不到 frame 議題——disclaimer frame 段落的字面可能全 positive、但整段仍是 prohibition。</p>
<p>補丁：輪 3 加一條 frame 檢查問題：「<strong>段落主體在告訴 reader 該做什麼、還是不該做什麼？</strong>」——告訴不該做 → reframe；告訴該做 → frame OK。</p>
<p>這條檢查放在輪 3 是因為 frame 議題跟「絕對主義語氣」同軸、都是「告訴 reader 規則 vs 教 reader 思考」的延伸。<strong>Frame 議題的根因檢查在輪 1 生成時更便宜</strong>（生成段落時就問 frame）、輪 3 是 fallback safety net。</p>
]]></content:encoded></item><item><title>術語翻譯要保留原文錨點</title><link>https://tarrragon.github.io/blog/report/terminology-keeps-original-anchor/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/terminology-keeps-original-anchor/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>術語翻譯要保留原文錨點：第一次出現用「中文術語（original term）」建立雙錨點，後續可依語境使用中文或原文。中文名稱負責讓段落可讀，原文名稱負責讓概念可回溯到來源、搜尋結果與跨語言討論。&lt;/p>
&lt;p>這次 &lt;code>paternalism&lt;/code> 被翻成「父權式保護」就是典型風險。中文詞把 reader 帶向 gender / patriarchy 的語意場，但原詞在倫理與決策脈絡裡更接近「家長主義」或「家長作風」：替他人決定什麼對他好。保留 &lt;code>paternalism&lt;/code> 讓 reviewer 能立刻發現中文錨點偏移。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼只留中文會漂移">為什麼只留中文會漂移&lt;/h2>
&lt;p>中文翻譯常同時承擔「自然語感」與「術語對位」兩個責任，兩者不一定一致。自然語感好的詞可能帶入額外文化語意；術語對位準的詞可能不夠口語。只留中文時，讀者看不到原概念邊界，reviewer 也難判斷翻譯是否偏移。&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>reviewer 能回到原始概念判斷翻譯是否準確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>工程方法論術語&lt;/td>
 &lt;td>中文詞看似通順、實際少了既有社群脈絡&lt;/td>
 &lt;td>reader 可用英文搜尋更多案例與反例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AI / 工具鏈新術語&lt;/td>
 &lt;td>中文翻譯尚未穩定、不同社群各翻一套&lt;/td>
 &lt;td>原文維持 grep / search / citation 可回溯性&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>翻譯是把 reader 從原文帶到中文語境。原文錨點是這條路的回程票。&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;code>父權式保護&lt;/code> 的失效點&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>「父權式」引入 gender / patriarchy 前提&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>動詞搭配&lt;/td>
 &lt;td>這個詞能自然承接後面的動作嗎？&lt;/td>
 &lt;td>「通過父權式保護 4 條件測試」語意卡住&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>reader 看到這個詞會問哪個問題？&lt;/td>
 &lt;td>會問「父權在哪」，不是問「自主性在哪」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這類錯誤容易犯，是因為第一版翻譯常由三個便利訊號驅動：字根聯想、上下文補完、中文順口度。三者都能產生看似合理的詞，但句內邏輯檢查會暴露它們是否真的承接原文概念。&lt;/p>
&lt;hr>
&lt;h2 id="case家長主義為什麼漂成父權式保護">Case：家長主義為什麼漂成父權式保護&lt;/h2>
&lt;p>&lt;code>paternalism&lt;/code> 容易被誤翻成「父權式保護」，是因為譯者同時看到 &lt;code>paternal&lt;/code> 的父系字根與「保護他人」的語境，於是把兩個表面訊號合成一個看似順口的中文詞。這個合成跳過了術語層檢查：&lt;code>paternalism&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>字根聯想&lt;/td>
 &lt;td>&lt;code>paternal&lt;/code> → 父親 / 父系&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>性別 / patriarchy 權力結構&lt;/td>
 &lt;td>自主性（autonomy）與介入正當性的邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reviewer 訊號&lt;/td>
 &lt;td>讀者問「這跟父權有什麼關係？」&lt;/td>
 &lt;td>回到 &lt;code>paternalism&lt;/code>，檢查常見譯名與學術用法&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「父權式保護」不是完全無法理解，但它放回句子後會讓邏輯多出一個沒有來源的前提：這裡似乎有父權或性別權力結構。原段落真正需要承接的是 autonomy、consent、intervention、best-interest justification；中文譯名一旦讓 reader 問「父權在哪」，就表示它沒有通過句內邏輯檢查。&lt;/p>
&lt;p>這個案例的修法是先定義概念責任，再選中文譯名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">家長主義（paternalism）在本文指「以對方利益之名限制對方自主」。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">常見譯名也包含「家長作風」；本文統一用「家長主義」。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這樣寫保留三個訊號：中文有穩定常見譯名、英文可回溯原概念、定義句把 reader 從「父權」導回「自主性與介入正當性」。&lt;/p>
&lt;hr>
&lt;h2 id="判斷什麼算術語">判斷什麼算術語&lt;/h2>
&lt;p>術語是跨段落、跨文件或跨社群會被重複引用的概念單位。判斷重點是讀者是否需要用同一個 label 追蹤同一個概念，跟它是不是英文無關。&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>中文術語（original term）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者可能需要搜尋外部案例&lt;/td>
 &lt;td>是術語&lt;/td>
 &lt;td>保留英文方便搜尋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中文翻譯存在多種常見說法&lt;/td>
 &lt;td>需要對位檢查&lt;/td>
 &lt;td>選一個中文 + 原文括號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只是普通英文動詞或工具操作&lt;/td>
 &lt;td>通常不是術語&lt;/td>
 &lt;td>直接中文化或保留工具名稱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>專案內部自造名詞且尚未穩定&lt;/td>
 &lt;td>先當候選術語&lt;/td>
 &lt;td>暫用中文 + 英文或 slug 錨點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>例如 &lt;code>paternalism&lt;/code>、&lt;code>narrow framing&lt;/code>、&lt;code>premortem&lt;/code>、&lt;code>dark pattern&lt;/code> 都是術語；&lt;code>run&lt;/code>、&lt;code>write&lt;/code>、&lt;code>read&lt;/code> 在一般句子裡多半不是術語。&lt;code>tool&lt;/code> 若在討論 LLM tool selection 的決策偏誤時，才升格為術語錨點。&lt;/p>
&lt;hr>
&lt;h2 id="寫作規則">寫作規則&lt;/h2>
&lt;p>術語第一次出現時用中文在前、英文在括號後：&lt;code>家長主義（paternalism）&lt;/code>。中文是讀者入口，英文是概念邊界。若英文是縮寫，第一次寫全名加縮寫：&lt;code>概念驗證（proof of concept, POC）&lt;/code>，後續再用 &lt;code>POC&lt;/code>。&lt;/p>
&lt;p>同一篇文章內保持一個中文譯名。若需要提到其他常見譯名，把它放在定義段，不在正文來回切換：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">家長主義（paternalism）在本文指「替他人決定什麼對他好」。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">常見譯名也包含「家長作風」；本文統一用「家長主義」。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>翻譯不確定時，保留原文比假裝中文已穩定更可靠。下一輪 review 可改中文譯名，但原文錨點能維持搜尋與 cross-link 穩定。&lt;/p>
&lt;hr>
&lt;h2 id="multi-pass-裡怎麼察覺翻譯邏輯錯位">Multi-pass 裡怎麼察覺翻譯邏輯錯位&lt;/h2>
&lt;p>翻譯檢查需要在多輪檢查中獨立成一個子 pass，重點不是問「這個中文順不順」，而是問「這個中文放回句子後，邏輯是否仍然成立」。一般命名 review 會問 grep、長度、一致性；翻譯邏輯 review 要額外問句內角色、修飾關係、因果方向與讀者追問。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pass&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>grep 英文括號、標題、表格第一欄、index entry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>原文錨點&lt;/td>
 &lt;td>第一次出現是否保留 original term？&lt;/td>
 &lt;td>補「中文術語（original term）」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>句內角色檢查&lt;/td>
 &lt;td>中文詞在句中扮演的角色是否對？&lt;/td>
 &lt;td>把句子主詞、動詞、受詞標出來，確認譯名能承接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修飾語檢查&lt;/td>
 &lt;td>中文修飾語是否引入原文沒有的前提？&lt;/td>
 &lt;td>問「這個修飾語從原文哪裡來？」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>因果方向檢查&lt;/td>
 &lt;td>譯名是否改變段落後面的推論方向？&lt;/td>
 &lt;td>用一句話說明「因為 X 所以 Y」，看 X 是否被換掉&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者追問檢查&lt;/td>
 &lt;td>reader 看到中文會追問正確的問題嗎？&lt;/td>
 &lt;td>問「他會問 A 還是 B？」；問錯方向就重譯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Surface 同步&lt;/td>
 &lt;td>metadata / navigation 是否也使用同一術語？&lt;/td>
 &lt;td>掃 title、description、heading、link label、MOC&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>翻譯 pass 的關鍵問題是：「這個中文詞是否讓句子說了一個原文沒有說的東西？」會的話，這是邏輯錯位，需要重譯。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>術語翻譯要保留原文錨點：第一次出現用「中文術語（original term）」建立雙錨點，後續可依語境使用中文或原文。中文名稱負責讓段落可讀，原文名稱負責讓概念可回溯到來源、搜尋結果與跨語言討論。</p>
<p>這次 <code>paternalism</code> 被翻成「父權式保護」就是典型風險。中文詞把 reader 帶向 gender / patriarchy 的語意場，但原詞在倫理與決策脈絡裡更接近「家長主義」或「家長作風」：替他人決定什麼對他好。保留 <code>paternalism</code> 讓 reviewer 能立刻發現中文錨點偏移。</p>
<hr>
<h2 id="為什麼只留中文會漂移">為什麼只留中文會漂移</h2>
<p>中文翻譯常同時承擔「自然語感」與「術語對位」兩個責任，兩者不一定一致。自然語感好的詞可能帶入額外文化語意；術語對位準的詞可能不夠口語。只留中文時，讀者看不到原概念邊界，reviewer 也難判斷翻譯是否偏移。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>只留中文的風險</th>
          <th>加原文錨點後的效果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>學術 / 倫理術語</td>
          <td>中文詞帶入不同學派或文化語意</td>
          <td>reviewer 能回到原始概念判斷翻譯是否準確</td>
      </tr>
      <tr>
          <td>工程方法論術語</td>
          <td>中文詞看似通順、實際少了既有社群脈絡</td>
          <td>reader 可用英文搜尋更多案例與反例</td>
      </tr>
      <tr>
          <td>AI / 工具鏈新術語</td>
          <td>中文翻譯尚未穩定、不同社群各翻一套</td>
          <td>原文維持 grep / search / citation 可回溯性</td>
      </tr>
      <tr>
          <td>跨文件共用核心概念</td>
          <td>各篇各自翻譯、同概念變多個中文名稱</td>
          <td>原文成為概念單一真實來源</td>
      </tr>
  </tbody>
</table>
<p>翻譯是把 reader 從原文帶到中文語境。原文錨點是這條路的回程票。</p>
<hr>
<h2 id="翻譯檢查先看句內邏輯">翻譯檢查先看句內邏輯</h2>
<p>翻譯問題先是句內邏輯問題，再是術語問題。把譯名放回原句後，要檢查它跟主詞、動詞、修飾語、因果關係是否成立；如果譯名讓句子多出原文沒有的前提，或讓後面的論證方向改變，這個翻譯就有問題。</p>
<table>
  <thead>
      <tr>
          <th>檢查層</th>
          <th>問題</th>
          <th><code>父權式保護</code> 的失效點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主詞角色</td>
          <td>這個詞描述的是誰的行為或關係？</td>
          <td>句子在談規則對自主性的介入，不是在談父權</td>
      </tr>
      <tr>
          <td>修飾語關係</td>
          <td>中文修飾語是否引入額外前提？</td>
          <td>「父權式」引入 gender / patriarchy 前提</td>
      </tr>
      <tr>
          <td>動詞搭配</td>
          <td>這個詞能自然承接後面的動作嗎？</td>
          <td>「通過父權式保護 4 條件測試」語意卡住</td>
      </tr>
      <tr>
          <td>因果方向</td>
          <td>這個詞是否支撐段落原本的因果？</td>
          <td>原因是「替對方決定」，不是「父權結構」</td>
      </tr>
      <tr>
          <td>讀者追問方向</td>
          <td>reader 看到這個詞會問哪個問題？</td>
          <td>會問「父權在哪」，不是問「自主性在哪」</td>
      </tr>
  </tbody>
</table>
<p>這類錯誤容易犯，是因為第一版翻譯常由三個便利訊號驅動：字根聯想、上下文補完、中文順口度。三者都能產生看似合理的詞，但句內邏輯檢查會暴露它們是否真的承接原文概念。</p>
<hr>
<h2 id="case家長主義為什麼漂成父權式保護">Case：家長主義為什麼漂成父權式保護</h2>
<p><code>paternalism</code> 容易被誤翻成「父權式保護」，是因為譯者同時看到 <code>paternal</code> 的父系字根與「保護他人」的語境，於是把兩個表面訊號合成一個看似順口的中文詞。這個合成跳過了術語層檢查：<code>paternalism</code> 在倫理、政治哲學與決策設計裡的核心是「以對方利益之名限制對方自主」，跟性別權力無關。</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>錯誤路徑</th>
          <th>合理路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>字根聯想</td>
          <td><code>paternal</code> → 父親 / 父系</td>
          <td>字根只提供線索，不直接決定術語譯名</td>
      </tr>
      <tr>
          <td>語境補完</td>
          <td>保護使用者 → 父權式保護</td>
          <td>替他人決定何者對他好 → 家長主義</td>
      </tr>
      <tr>
          <td>概念責任</td>
          <td>性別 / patriarchy 權力結構</td>
          <td>自主性（autonomy）與介入正當性的邊界</td>
      </tr>
      <tr>
          <td>reviewer 訊號</td>
          <td>讀者問「這跟父權有什麼關係？」</td>
          <td>回到 <code>paternalism</code>，檢查常見譯名與學術用法</td>
      </tr>
  </tbody>
</table>
<p>「父權式保護」不是完全無法理解，但它放回句子後會讓邏輯多出一個沒有來源的前提：這裡似乎有父權或性別權力結構。原段落真正需要承接的是 autonomy、consent、intervention、best-interest justification；中文譯名一旦讓 reader 問「父權在哪」，就表示它沒有通過句內邏輯檢查。</p>
<p>這個案例的修法是先定義概念責任，再選中文譯名：</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">家長主義（paternalism）在本文指「以對方利益之名限制對方自主」。
</span></span><span class="line"><span class="ln">2</span><span class="cl">常見譯名也包含「家長作風」；本文統一用「家長主義」。</span></span></code></pre></div><p>這樣寫保留三個訊號：中文有穩定常見譯名、英文可回溯原概念、定義句把 reader 從「父權」導回「自主性與介入正當性」。</p>
<hr>
<h2 id="判斷什麼算術語">判斷什麼算術語</h2>
<p>術語是跨段落、跨文件或跨社群會被重複引用的概念單位。判斷重點是讀者是否需要用同一個 label 追蹤同一個概念，跟它是不是英文無關。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判斷</th>
          <th>寫法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>來自學術、標準、框架、方法論</td>
          <td>是術語</td>
          <td>中文術語（original term）</td>
      </tr>
      <tr>
          <td>讀者可能需要搜尋外部案例</td>
          <td>是術語</td>
          <td>保留英文方便搜尋</td>
      </tr>
      <tr>
          <td>中文翻譯存在多種常見說法</td>
          <td>需要對位檢查</td>
          <td>選一個中文 + 原文括號</td>
      </tr>
      <tr>
          <td>只是普通英文動詞或工具操作</td>
          <td>通常不是術語</td>
          <td>直接中文化或保留工具名稱</td>
      </tr>
      <tr>
          <td>專案內部自造名詞且尚未穩定</td>
          <td>先當候選術語</td>
          <td>暫用中文 + 英文或 slug 錨點</td>
      </tr>
  </tbody>
</table>
<p>例如 <code>paternalism</code>、<code>narrow framing</code>、<code>premortem</code>、<code>dark pattern</code> 都是術語；<code>run</code>、<code>write</code>、<code>read</code> 在一般句子裡多半不是術語。<code>tool</code> 若在討論 LLM tool selection 的決策偏誤時，才升格為術語錨點。</p>
<hr>
<h2 id="寫作規則">寫作規則</h2>
<p>術語第一次出現時用中文在前、英文在括號後：<code>家長主義（paternalism）</code>。中文是讀者入口，英文是概念邊界。若英文是縮寫，第一次寫全名加縮寫：<code>概念驗證（proof of concept, POC）</code>，後續再用 <code>POC</code>。</p>
<p>同一篇文章內保持一個中文譯名。若需要提到其他常見譯名，把它放在定義段，不在正文來回切換：</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">家長主義（paternalism）在本文指「替他人決定什麼對他好」。
</span></span><span class="line"><span class="ln">2</span><span class="cl">常見譯名也包含「家長作風」；本文統一用「家長主義」。</span></span></code></pre></div><p>翻譯不確定時，保留原文比假裝中文已穩定更可靠。下一輪 review 可改中文譯名，但原文錨點能維持搜尋與 cross-link 穩定。</p>
<hr>
<h2 id="multi-pass-裡怎麼察覺翻譯邏輯錯位">Multi-pass 裡怎麼察覺翻譯邏輯錯位</h2>
<p>翻譯檢查需要在多輪檢查中獨立成一個子 pass，重點不是問「這個中文順不順」，而是問「這個中文放回句子後，邏輯是否仍然成立」。一般命名 review 會問 grep、長度、一致性；翻譯邏輯 review 要額外問句內角色、修飾關係、因果方向與讀者追問。</p>
<table>
  <thead>
      <tr>
          <th>Pass</th>
          <th>問題</th>
          <th>操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>術語枚舉</td>
          <td>哪些中文詞是由英文概念翻來的？</td>
          <td>grep 英文括號、標題、表格第一欄、index entry</td>
      </tr>
      <tr>
          <td>原文錨點</td>
          <td>第一次出現是否保留 original term？</td>
          <td>補「中文術語（original term）」</td>
      </tr>
      <tr>
          <td>句內角色檢查</td>
          <td>中文詞在句中扮演的角色是否對？</td>
          <td>把句子主詞、動詞、受詞標出來，確認譯名能承接</td>
      </tr>
      <tr>
          <td>修飾語檢查</td>
          <td>中文修飾語是否引入原文沒有的前提？</td>
          <td>問「這個修飾語從原文哪裡來？」</td>
      </tr>
      <tr>
          <td>因果方向檢查</td>
          <td>譯名是否改變段落後面的推論方向？</td>
          <td>用一句話說明「因為 X 所以 Y」，看 X 是否被換掉</td>
      </tr>
      <tr>
          <td>讀者追問檢查</td>
          <td>reader 看到中文會追問正確的問題嗎？</td>
          <td>問「他會問 A 還是 B？」；問錯方向就重譯</td>
      </tr>
      <tr>
          <td>Surface 同步</td>
          <td>metadata / navigation 是否也使用同一術語？</td>
          <td>掃 title、description、heading、link label、MOC</td>
      </tr>
  </tbody>
</table>
<p>翻譯 pass 的關鍵問題是：「這個中文詞是否讓句子說了一個原文沒有說的東西？」會的話，這是邏輯錯位，需要重譯。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只留中文、不留原文</td>
          <td>翻譯偏移時 reviewer 難發現</td>
          <td>第一次出現補原文括號</td>
      </tr>
      <tr>
          <td>只留英文、不給中文</td>
          <td>讀者每段都要自行翻譯、閱讀負擔升高</td>
          <td>補中文入口</td>
      </tr>
      <tr>
          <td>同一術語多個中文譯名混用</td>
          <td>reader 以為是多個概念</td>
          <td>選一個 canonical 中文譯名</td>
      </tr>
      <tr>
          <td>中文譯名帶入額外政治 / 文化場</td>
          <td>概念邊界被不相關語意污染</td>
          <td>回原文檢查語境，必要時換譯名</td>
      </tr>
      <tr>
          <td>英文括號放在多個不同中文後面</td>
          <td>表示中文端沒有 SSoT，搜尋與理解都會分裂</td>
          <td>建術語表或在段首定義 canonical</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>術語翻譯是命名的一種；第一版中文譯名常基於當下語境，必須接受 reviewer 修正。</td>
      </tr>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></td>
          <td>title、description、index entry 若使用術語，也要保留同一組中文 / 原文錨點。</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 Single Source of Truth</a></td>
          <td>原文術語可作為跨中文譯名的概念 SSoT；中文譯名可以調整，原概念錨點不可漂移。</td>
      </tr>
      <tr>
          <td><a href="../security-citation-currency-and-precision/">#104 Security 標準引用的時效性與精確度</a></td>
          <td>高 stakes 領域的標準術語若翻譯漂移，會把 conditional / scope 一起翻錯；原文錨點降低扭曲風險。</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>reviewer 問「這個中文是什麼意思」</td>
          <td>補原文錨點，重新檢查譯名是否對位</td>
      </tr>
      <tr>
          <td>同一英文可翻成兩個以上中文</td>
          <td>在定義段列常見譯名，正文選一個 canonical</td>
      </tr>
      <tr>
          <td>中文詞帶出不相關聯想</td>
          <td>回原文語境，換成較中性的中文</td>
      </tr>
      <tr>
          <td>文章需要讀者搜尋外部資料</td>
          <td>第一次出現保留英文，讓搜尋 query 可直接使用</td>
      </tr>
      <tr>
          <td>術語出現在標題 / index entry</td>
          <td>metadata surface 也要補雙錨點，不只正文補</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：術語翻譯是概念對位，不是字面替換。中文讓文章可讀，原文讓概念可查、可驗、可被修正。少任一邊，reader 都會付額外成本。</p>
]]></content:encoded></item><item><title>中文壓縮術語要保留完整名詞頭</title><link>https://tarrragon.github.io/blog/report/compressed-chinese-terms-need-head-noun/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/compressed-chinese-terms-need-head-noun/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>中文壓縮術語要保留完整名詞頭：壓縮後的詞仍要能獨立回答「這是什麼」。技術文章可以把長概念縮短，但縮短後至少保留一個明確 head noun，例如「盲點」「風險」「偏誤」「檢查」「模式」「策略」。&lt;/p>
&lt;p>這次「多步驟 perplexity 盲」的問題在中文最後只剩「盲」一個字，中英混用本身不構成障礙。讀者無法判斷它是「盲點」「盲區」「盲測」還是形容詞式比喻；改成「多步驟成功率盲點」後，概念角色才完整：它是一種盲點，不是一個未完成的片語。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼單字壓縮會斷語意">為什麼單字壓縮會斷語意&lt;/h2>
&lt;p>中文可以高度省略，但技術寫作的省略成本由 reader 承擔。口語裡靠語境補完的詞，在文件裡會被單獨搜尋、引用、放進表格、出現在 index entry。當詞失去名詞頭，離開原句就失去語意。&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>多步驟 perplexity 盲&lt;/td>
 &lt;td>「盲」不是穩定名詞頭&lt;/td>
 &lt;td>多步驟成功率盲點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Anchor&lt;/td>
 &lt;td>不知是錨定、錨點、anchor check&lt;/td>
 &lt;td>既有結論錨定（Anchor）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>autopilot&lt;/td>
 &lt;td>不知是狀態、模式、失敗類型&lt;/td>
 &lt;td>自動駕駛模式（autopilot）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>POC&lt;/td>
 &lt;td>不知是文件、測試、最小實作&lt;/td>
 &lt;td>概念驗證（POC）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>toolification&lt;/td>
 &lt;td>不知是行為、偏誤、設計策略&lt;/td>
 &lt;td>工具化偏誤（toolification）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>壓縮後的詞會被拿去做段首、表格列名、索引條與 grep query。它必須在這些位置都能獨立成立。&lt;/p>
&lt;hr>
&lt;h2 id="head-noun-檢查">Head noun 檢查&lt;/h2>
&lt;p>Head noun 是術語最後承擔分類責任的名詞。它告訴 reader 這個概念屬於哪一類東西：偏誤、風險、模式、策略、檢查、條件、訊號、盲點。&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>知道 → head noun 足夠；不知道 → 補名詞頭&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>這個詞能放進表格第一欄嗎？&lt;/td>
 &lt;td>可以 → 可作分類；不可以 → 只是句中修飾片段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reader 能用它問問題嗎？&lt;/td>
 &lt;td>「如何處理 X 盲點」成立；「如何處理 X 盲」不成立&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>它能跟同類詞並列嗎？&lt;/td>
 &lt;td>「偏誤 / 風險 / 盲點」可並列；單字修飾不可並列&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>常用 head noun 應該具體對應概念責任：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Head noun&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>盲點&lt;/td>
 &lt;td>決策者看不到的資訊或推論缺口&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>偏誤&lt;/td>
 &lt;td>系統性推理傾向&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>風險&lt;/td>
 &lt;td>可能造成損失但尚未發生的條件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>模式&lt;/td>
 &lt;td>反覆出現的行為結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>檢查&lt;/td>
 &lt;td>可操作的 review 動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>策略&lt;/td>
 &lt;td>可選擇的處理路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訊號&lt;/td>
 &lt;td>用來判讀何時觸發下一步的觀察&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="改寫路徑">改寫路徑&lt;/h2>
&lt;p>改寫壓縮詞時，先判斷它要扮演哪種概念角色，再補 head noun，不先追求短。&lt;/p>
&lt;ol>
&lt;li>先問：「這是偏誤、風險、盲點、模式，還是檢查？」&lt;/li>
&lt;li>保留最能定位來源的核心詞。&lt;/li>
&lt;li>把英文原詞放在中文後括號，避免中文壓縮造成歧義。&lt;/li>
&lt;li>對同一篇文章跑 grep，確認同概念沒有多個中文變體。&lt;/li>
&lt;/ol>
&lt;p>例如「多步驟 perplexity 盲」可拆成：&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>perplexity&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>缺 head noun、原文也不精準&lt;/td>
 &lt;td>多步驟成功率盲點&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>若原文術語本身重要，寫成「多步驟成功率盲點（multi-step success-rate blind spot）」比硬塞半段英文更穩。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>為了短，刪掉最後名詞頭&lt;/td>
 &lt;td>reader 不知道概念類型&lt;/td>
 &lt;td>補「盲點 / 偏誤 / 風險」等類型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中英混在同一個未完成片語&lt;/td>
 &lt;td>中英文都無法提供完整語意&lt;/td>
 &lt;td>中文完整名詞 + 英文括號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>表格欄位用口語縮寫&lt;/td>
 &lt;td>離開上下文後不可讀&lt;/td>
 &lt;td>欄位名稱完整化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用流行詞取代分類詞&lt;/td>
 &lt;td>看起來有風格，實際無法判斷下一步&lt;/td>
 &lt;td>改成可操作分類&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>每處各自補完不同名詞頭&lt;/td>
 &lt;td>同一概念變成多個術語&lt;/td>
 &lt;td>選 canonical 名詞頭後全篇統一&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>&lt;a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact&lt;/a>&lt;/td>
 &lt;td>壓縮術語是命名問題；第一版為了快常少 head noun，第二輪要從 reader 與 grep 角度重命名。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review&lt;/a>&lt;/td>
 &lt;td>這是輪 4 grep-ability / 命名的子檢查；術語能否獨立搜尋與引用，要另跑一眼。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點&lt;/a>&lt;/td>
 &lt;td>原文錨點解決概念來源，head noun 解決中文句內可讀性；兩者要一起做。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>短詞通常比較好寫，但不一定對齊讀者理解；完整名詞頭是用字數換認知穩定。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>reviewer 問「這個 X 是什麼」&lt;/td>
 &lt;td>補 head noun，讓術語自己回答概念類型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>術語最後一字是形容詞或單字&lt;/td>
 &lt;td>檢查是否缺「點 / 區 / 型 / 法 / 策略」等名詞&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>表格第一欄看起來像句子殘片&lt;/td>
 &lt;td>改成完整分類名詞&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一概念被補成多種詞尾&lt;/td>
 &lt;td>選 canonical head noun，全篇替換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中英混用後仍不能搜尋&lt;/td>
 &lt;td>改成中文完整術語 + 英文完整原詞&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：壓縮是最後一步，不是第一步。術語先要完整、可分類、可搜尋，再決定能不能縮短。少了 head noun 的短詞不是精煉，是把補完成本丟給 reader。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>中文壓縮術語要保留完整名詞頭：壓縮後的詞仍要能獨立回答「這是什麼」。技術文章可以把長概念縮短，但縮短後至少保留一個明確 head noun，例如「盲點」「風險」「偏誤」「檢查」「模式」「策略」。</p>
<p>這次「多步驟 perplexity 盲」的問題在中文最後只剩「盲」一個字，中英混用本身不構成障礙。讀者無法判斷它是「盲點」「盲區」「盲測」還是形容詞式比喻；改成「多步驟成功率盲點」後，概念角色才完整：它是一種盲點，不是一個未完成的片語。</p>
<hr>
<h2 id="為什麼單字壓縮會斷語意">為什麼單字壓縮會斷語意</h2>
<p>中文可以高度省略，但技術寫作的省略成本由 reader 承擔。口語裡靠語境補完的詞，在文件裡會被單獨搜尋、引用、放進表格、出現在 index entry。當詞失去名詞頭，離開原句就失去語意。</p>
<table>
  <thead>
      <tr>
          <th>壓縮形式</th>
          <th>問題</th>
          <th>完整形式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多步驟 perplexity 盲</td>
          <td>「盲」不是穩定名詞頭</td>
          <td>多步驟成功率盲點</td>
      </tr>
      <tr>
          <td>Anchor</td>
          <td>不知是錨定、錨點、anchor check</td>
          <td>既有結論錨定（Anchor）</td>
      </tr>
      <tr>
          <td>autopilot</td>
          <td>不知是狀態、模式、失敗類型</td>
          <td>自動駕駛模式（autopilot）</td>
      </tr>
      <tr>
          <td>POC</td>
          <td>不知是文件、測試、最小實作</td>
          <td>概念驗證（POC）</td>
      </tr>
      <tr>
          <td>toolification</td>
          <td>不知是行為、偏誤、設計策略</td>
          <td>工具化偏誤（toolification）</td>
      </tr>
  </tbody>
</table>
<p>壓縮後的詞會被拿去做段首、表格列名、索引條與 grep query。它必須在這些位置都能獨立成立。</p>
<hr>
<h2 id="head-noun-檢查">Head noun 檢查</h2>
<p>Head noun 是術語最後承擔分類責任的名詞。它告訴 reader 這個概念屬於哪一類東西：偏誤、風險、模式、策略、檢查、條件、訊號、盲點。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>判斷</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這個詞拿出句子後還知道是什麼嗎？</td>
          <td>知道 → head noun 足夠；不知道 → 補名詞頭</td>
      </tr>
      <tr>
          <td>這個詞能放進表格第一欄嗎？</td>
          <td>可以 → 可作分類；不可以 → 只是句中修飾片段</td>
      </tr>
      <tr>
          <td>reader 能用它問問題嗎？</td>
          <td>「如何處理 X 盲點」成立；「如何處理 X 盲」不成立</td>
      </tr>
      <tr>
          <td>它能跟同類詞並列嗎？</td>
          <td>「偏誤 / 風險 / 盲點」可並列；單字修飾不可並列</td>
      </tr>
  </tbody>
</table>
<p>常用 head noun 應該具體對應概念責任：</p>
<table>
  <thead>
      <tr>
          <th>Head noun</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>
      <tr>
          <td>檢查</td>
          <td>可操作的 review 動作</td>
      </tr>
      <tr>
          <td>策略</td>
          <td>可選擇的處理路徑</td>
      </tr>
      <tr>
          <td>訊號</td>
          <td>用來判讀何時觸發下一步的觀察</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="改寫路徑">改寫路徑</h2>
<p>改寫壓縮詞時，先判斷它要扮演哪種概念角色，再補 head noun，不先追求短。</p>
<ol>
<li>先問：「這是偏誤、風險、盲點、模式，還是檢查？」</li>
<li>保留最能定位來源的核心詞。</li>
<li>把英文原詞放在中文後括號，避免中文壓縮造成歧義。</li>
<li>對同一篇文章跑 grep，確認同概念沒有多個中文變體。</li>
</ol>
<p>例如「多步驟 perplexity 盲」可拆成：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>問題</th>
          <th>改寫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多步驟</td>
          <td>保留，說明失效情境</td>
          <td>多步驟</td>
      </tr>
      <tr>
          <td>perplexity</td>
          <td>在此語境其實指成功率估計</td>
          <td>成功率</td>
      </tr>
      <tr>
          <td>盲</td>
          <td>單字不完整</td>
          <td>盲點</td>
      </tr>
      <tr>
          <td>完整術語</td>
          <td>缺 head noun、原文也不精準</td>
          <td>多步驟成功率盲點</td>
      </tr>
  </tbody>
</table>
<p>若原文術語本身重要，寫成「多步驟成功率盲點（multi-step success-rate blind spot）」比硬塞半段英文更穩。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>為了短，刪掉最後名詞頭</td>
          <td>reader 不知道概念類型</td>
          <td>補「盲點 / 偏誤 / 風險」等類型</td>
      </tr>
      <tr>
          <td>中英混在同一個未完成片語</td>
          <td>中英文都無法提供完整語意</td>
          <td>中文完整名詞 + 英文括號</td>
      </tr>
      <tr>
          <td>表格欄位用口語縮寫</td>
          <td>離開上下文後不可讀</td>
          <td>欄位名稱完整化</td>
      </tr>
      <tr>
          <td>用流行詞取代分類詞</td>
          <td>看起來有風格，實際無法判斷下一步</td>
          <td>改成可操作分類</td>
      </tr>
      <tr>
          <td>每處各自補完不同名詞頭</td>
          <td>同一概念變成多個術語</td>
          <td>選 canonical 名詞頭後全篇統一</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>壓縮術語是命名問題；第一版為了快常少 head noun，第二輪要從 reader 與 grep 角度重命名。</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td>這是輪 4 grep-ability / 命名的子檢查；術語能否獨立搜尋與引用，要另跑一眼。</td>
      </tr>
      <tr>
          <td><a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點</a></td>
          <td>原文錨點解決概念來源，head noun 解決中文句內可讀性；兩者要一起做。</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>短詞通常比較好寫，但不一定對齊讀者理解；完整名詞頭是用字數換認知穩定。</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>reviewer 問「這個 X 是什麼」</td>
          <td>補 head noun，讓術語自己回答概念類型</td>
      </tr>
      <tr>
          <td>術語最後一字是形容詞或單字</td>
          <td>檢查是否缺「點 / 區 / 型 / 法 / 策略」等名詞</td>
      </tr>
      <tr>
          <td>表格第一欄看起來像句子殘片</td>
          <td>改成完整分類名詞</td>
      </tr>
      <tr>
          <td>同一概念被補成多種詞尾</td>
          <td>選 canonical head noun，全篇替換</td>
      </tr>
      <tr>
          <td>中英混用後仍不能搜尋</td>
          <td>改成中文完整術語 + 英文完整原詞</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：壓縮是最後一步，不是第一步。術語先要完整、可分類、可搜尋，再決定能不能縮短。少了 head noun 的短詞不是精煉，是把補完成本丟給 reader。</p>
]]></content:encoded></item><item><title>術語翻譯要保留概念角色</title><link>https://tarrragon.github.io/blog/report/translation-must-preserve-concept-role/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/translation-must-preserve-concept-role/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>術語翻譯要保留概念角色：先判斷原詞在段落中承擔的是「論證方法」「檢查動作」「偏誤名稱」還是「流程階段」，再選中文名詞頭。中文譯名如果改變了概念類型，即使句子仍然順口，也會讓 reader 用錯誤方式理解後續操作。&lt;/p>
&lt;p>這次 &lt;code>Steelman&lt;/code> 被翻成「最強版本測試」就是這類問題。這個譯名抓到 WRAP checklist 裡的使用情境，卻把概念角色從「重建對方最強論點」壓成「通過一個測試」。較穩的寫法是「最強版本論證（Steelman）」：中文保留論證角色，英文保留可回溯來源。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼概念角色會被翻掉">為什麼概念角色會被翻掉&lt;/h2>
&lt;p>翻譯容易把術語放到當下文件的局部用途裡命名。當術語出現在 checklist、流程表或標題中，譯者會自然把它翻成「測試」「檢查」「步驟」；但原詞可能是一種論證姿態或推理方法，被壓成了測試動作。&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;code>Steelman&lt;/code> 壓成 checklist 動作&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>不利於對照 steelmanning 社群用法&lt;/td>
 &lt;td>可回到 &lt;code>Steelman&lt;/code> / &lt;code>steelmanning&lt;/code>&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;code>Steelman&lt;/code> 的核心不是「測出是否正確」，而是「把反方論點講到比原支持者更完整」。&lt;a href="https://www.lesswrong.com/w/steelmanning">LessWrong 的 steelmanning 條目&lt;/a>與 &lt;a href="https://en.wiktionary.org/wiki/steelman">Wiktionary 的 steelman 定義&lt;/a>都把它放在論證與辯論脈絡，而不是一般測試脈絡。中文若只寫「測試」，會把 reader 的注意力移到通過條件，而不是反方論證的重建品質。&lt;/p>
&lt;hr>
&lt;h2 id="casesteelman-為什麼不宜翻成最強版本測試">Case：Steelman 為什麼不宜翻成最強版本測試&lt;/h2>
&lt;p>&lt;code>Steelman&lt;/code> 常被中文社群譯成「鋼人論證」或「鋼鐵人論證」，也常直接保留英文。這些譯名各有成本：「鋼鐵人」容易被聯想到角色 IP，「鋼人」對未接觸者不直觀；但它們至少保留了「論證」這個角色。若改成「最強版本測試」，中文更直覺，卻把概念從論證方法改成檢核動作。&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>鋼人論證（Steelman）&lt;/td>
 &lt;td>貼近 steelman 對偶&lt;/td>
 &lt;td>中文不夠直觀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>鋼鐵人論證（Steelman）&lt;/td>
 &lt;td>常見、容易記&lt;/td>
 &lt;td>可能引入 Iron Man 聯想&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>最強版本論證（Steelman）&lt;/td>
 &lt;td>直接說出操作與角色&lt;/td>
 &lt;td>不是最常見固定譯名&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>最強版本測試（Steelman）&lt;/td>
 &lt;td>適合 checklist 語境&lt;/td>
 &lt;td>把論證方法誤壓成測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>建構對方論點的最強版本&lt;/td>
 &lt;td>最清楚&lt;/td>
 &lt;td>太長，不適合作為表格欄位&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>因此在 WRAP 文章中，canonical 用語採「最強版本論證（Steelman）」。第一次出現可補一句定義：「本文指先把被放棄選項或反方立場重建成最有力版本，再檢查自己的選擇是否仍站得住腳。」後續 checklist 可以寫「最強版本論證是否完成」，而不是把術語本身改成「測試」。&lt;/p>
&lt;hr>
&lt;h2 id="檢查流程">檢查流程&lt;/h2>
&lt;p>術語翻譯的概念角色檢查，要放在「句內邏輯」之後、「surface 同步」之前。先確認中文沒有多出原文沒有的前提，再確認中文名詞頭沒有把概念換類。&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>因為放在 checklist，就翻成「測試」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者追問&lt;/td>
 &lt;td>reader 看到中文會追問哪件事？&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>Surface 面&lt;/td>
 &lt;td>title、heading、表格欄位是否傳播同一概念角色？&lt;/td>
 &lt;td>正文改對，checklist 仍保留舊角色&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最低修法是：中文名詞頭要對應概念角色，英文括號要保留來源錨點。若中文沒有穩定固定譯名，採解釋型中文也可以，但不能把方法翻成測試、把偏誤翻成狀態、把流程階段翻成工具。&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>&lt;a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點&lt;/a>&lt;/td>
 &lt;td>原文錨點讓 reviewer 能回到來源；本卡補「中文名詞頭要保留來源裡的概念角色」。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../compressed-chinese-terms-need-head-noun/">#108 中文壓縮術語要保留完整名詞頭&lt;/a>&lt;/td>
 &lt;td>#108 處理名詞頭缺失；本卡處理名詞頭存在但類型錯誤，例如把「論證」改成「測試」。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact&lt;/a>&lt;/td>
 &lt;td>術語中文名是命名成果；第一版常被局部語境拉偏，需要在第二輪用來源角色與本文角色重新校準。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍&lt;/a>&lt;/td>
 &lt;td>術語角色錯誤常殘留在 heading、checklist、index entry；surface review 要同步掃概念角色。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>術語出現在 checklist 就被翻成測試&lt;/td>
 &lt;td>回來源確認它是否本來就是 test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reviewer 問「這是測什麼」&lt;/td>
 &lt;td>檢查中文名詞頭是否把方法誤改成檢核動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中文譯名比原文多一個操作類型&lt;/td>
 &lt;td>問這個操作類型是否來自原文，而非本文版面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一術語在標題與表格中用不同名詞頭&lt;/td>
 &lt;td>選 canonical 角色，全篇同步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>常見譯名不直觀，解釋型譯名較清楚&lt;/td>
 &lt;td>保留英文括號，中文用「說明角色」而非硬直譯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心&lt;/strong>：翻譯術語時，中文要讓 reader 知道「這個概念是哪一類東西」。概念角色一旦翻錯，後續 checklist 再完整，也是在檢查錯的東西。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>術語翻譯要保留概念角色：先判斷原詞在段落中承擔的是「論證方法」「檢查動作」「偏誤名稱」還是「流程階段」，再選中文名詞頭。中文譯名如果改變了概念類型，即使句子仍然順口，也會讓 reader 用錯誤方式理解後續操作。</p>
<p>這次 <code>Steelman</code> 被翻成「最強版本測試」就是這類問題。這個譯名抓到 WRAP checklist 裡的使用情境，卻把概念角色從「重建對方最強論點」壓成「通過一個測試」。較穩的寫法是「最強版本論證（Steelman）」：中文保留論證角色，英文保留可回溯來源。</p>
<hr>
<h2 id="為什麼概念角色會被翻掉">為什麼概念角色會被翻掉</h2>
<p>翻譯容易把術語放到當下文件的局部用途裡命名。當術語出現在 checklist、流程表或標題中，譯者會自然把它翻成「測試」「檢查」「步驟」；但原詞可能是一種論證姿態或推理方法，被壓成了測試動作。</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>「最強版本測試」的問題</th>
          <th>「最強版本論證」保留的角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>概念類型</td>
          <td>把 <code>Steelman</code> 壓成 checklist 動作</td>
          <td>保留「建構最強反方論點」的方法角色</td>
      </tr>
      <tr>
          <td>動詞搭配</td>
          <td>讀者會問「怎樣算測試通過」</td>
          <td>讀者會問「有沒有公平重建反方論點」</td>
      </tr>
      <tr>
          <td>來源對位</td>
          <td>不利於對照 steelmanning 社群用法</td>
          <td>可回到 <code>Steelman</code> / <code>steelmanning</code></td>
      </tr>
      <tr>
          <td>後續推論</td>
          <td>容易只做形式檢查</td>
          <td>會回到理解反方與降低確認偏誤</td>
      </tr>
  </tbody>
</table>
<p><code>Steelman</code> 的核心不是「測出是否正確」，而是「把反方論點講到比原支持者更完整」。<a href="https://www.lesswrong.com/w/steelmanning">LessWrong 的 steelmanning 條目</a>與 <a href="https://en.wiktionary.org/wiki/steelman">Wiktionary 的 steelman 定義</a>都把它放在論證與辯論脈絡，而不是一般測試脈絡。中文若只寫「測試」，會把 reader 的注意力移到通過條件，而不是反方論證的重建品質。</p>
<hr>
<h2 id="casesteelman-為什麼不宜翻成最強版本測試">Case：Steelman 為什麼不宜翻成最強版本測試</h2>
<p><code>Steelman</code> 常被中文社群譯成「鋼人論證」或「鋼鐵人論證」，也常直接保留英文。這些譯名各有成本：「鋼鐵人」容易被聯想到角色 IP，「鋼人」對未接觸者不直觀；但它們至少保留了「論證」這個角色。若改成「最強版本測試」，中文更直覺，卻把概念從論證方法改成檢核動作。</p>
<table>
  <thead>
      <tr>
          <th>譯法</th>
          <th>優點</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>鋼人論證（Steelman）</td>
          <td>貼近 steelman 對偶</td>
          <td>中文不夠直觀</td>
      </tr>
      <tr>
          <td>鋼鐵人論證（Steelman）</td>
          <td>常見、容易記</td>
          <td>可能引入 Iron Man 聯想</td>
      </tr>
      <tr>
          <td>最強版本論證（Steelman）</td>
          <td>直接說出操作與角色</td>
          <td>不是最常見固定譯名</td>
      </tr>
      <tr>
          <td>最強版本測試（Steelman）</td>
          <td>適合 checklist 語境</td>
          <td>把論證方法誤壓成測試</td>
      </tr>
      <tr>
          <td>建構對方論點的最強版本</td>
          <td>最清楚</td>
          <td>太長，不適合作為表格欄位</td>
      </tr>
  </tbody>
</table>
<p>因此在 WRAP 文章中，canonical 用語採「最強版本論證（Steelman）」。第一次出現可補一句定義：「本文指先把被放棄選項或反方立場重建成最有力版本，再檢查自己的選擇是否仍站得住腳。」後續 checklist 可以寫「最強版本論證是否完成」，而不是把術語本身改成「測試」。</p>
<hr>
<h2 id="檢查流程">檢查流程</h2>
<p>術語翻譯的概念角色檢查，要放在「句內邏輯」之後、「surface 同步」之前。先確認中文沒有多出原文沒有的前提，再確認中文名詞頭沒有把概念換類。</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>因為放在 checklist，就翻成「測試」</td>
      </tr>
      <tr>
          <td>讀者追問</td>
          <td>reader 看到中文會追問哪件事？</td>
          <td>追問通過條件，而不是追問論證品質</td>
      </tr>
      <tr>
          <td>動詞搭配</td>
          <td>中文能承接本文要求的動作嗎？</td>
          <td>「做測試」可行，但「重建論點」消失</td>
      </tr>
      <tr>
          <td>Surface 面</td>
          <td>title、heading、表格欄位是否傳播同一概念角色？</td>
          <td>正文改對，checklist 仍保留舊角色</td>
      </tr>
  </tbody>
</table>
<p>最低修法是：中文名詞頭要對應概念角色，英文括號要保留來源錨點。若中文沒有穩定固定譯名，採解釋型中文也可以，但不能把方法翻成測試、把偏誤翻成狀態、把流程階段翻成工具。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點</a></td>
          <td>原文錨點讓 reviewer 能回到來源；本卡補「中文名詞頭要保留來源裡的概念角色」。</td>
      </tr>
      <tr>
          <td><a href="../compressed-chinese-terms-need-head-noun/">#108 中文壓縮術語要保留完整名詞頭</a></td>
          <td>#108 處理名詞頭缺失；本卡處理名詞頭存在但類型錯誤，例如把「論證」改成「測試」。</td>
      </tr>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>術語中文名是命名成果；第一版常被局部語境拉偏，需要在第二輪用來源角色與本文角色重新校準。</td>
      </tr>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></td>
          <td>術語角色錯誤常殘留在 heading、checklist、index entry；surface review 要同步掃概念角色。</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>術語出現在 checklist 就被翻成測試</td>
          <td>回來源確認它是否本來就是 test</td>
      </tr>
      <tr>
          <td>reviewer 問「這是測什麼」</td>
          <td>檢查中文名詞頭是否把方法誤改成檢核動作</td>
      </tr>
      <tr>
          <td>中文譯名比原文多一個操作類型</td>
          <td>問這個操作類型是否來自原文，而非本文版面</td>
      </tr>
      <tr>
          <td>同一術語在標題與表格中用不同名詞頭</td>
          <td>選 canonical 角色，全篇同步</td>
      </tr>
      <tr>
          <td>常見譯名不直觀，解釋型譯名較清楚</td>
          <td>保留英文括號，中文用「說明角色」而非硬直譯</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：翻譯術語時，中文要讓 reader 知道「這個概念是哪一類東西」。概念角色一旦翻錯，後續 checklist 再完整，也是在檢查錯的東西。</p>
]]></content:encoded></item><item><title>設計檢討用當下三軸論證、不依賴 hindsight</title><link>https://tarrragon.github.io/blog/report/design-flaw-by-current-axes-not-hindsight/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/design-flaw-by-current-axes-not-hindsight/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>1 個 case&lt;/strong>（&lt;a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故&lt;/a> 的 review 過程）抽出來的假說、不是經過多個獨立 case 驗證的工程原則。具體限制：&lt;/p>
&lt;ul>
&lt;li>「成本對稱性 / 可逆性 / 領域先驗」三軸框架借自 yagni 篇的判斷工具、不是 industry standard&lt;/li>
&lt;li>「hindsight 偏向個人歸因 / 三軸偏向制度歸因」是觀察性分歧、不是必然對立——實際事故報告（aviation / medical incident）也常用 hindsight 同時歸因到制度&lt;/li>
&lt;li>5 個多面向 case（code review / post-mortem / ADR / retrospective / 寫作）是假設性對比、未驗證真實案例中兩種論述的差異&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、把它當「&lt;strong>這個 blog 提倡的判定方式&lt;/strong>」、不當「驗證過的工程原則」。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>本卡提倡的「設計缺陷」判定方式是「&lt;strong>在當下成本對稱條件下選了限制更高的選項&lt;/strong>」、用以替代依賴結局的 hindsight 判定。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>事後諸葛論述（hindsight）&lt;/th>
 &lt;th>當下三軸論證（current axes）&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>後來真的需要 X、所以當初該做 X&lt;/td>
 &lt;td>當下三軸（成本對稱性 / 可逆性 / 領域先驗）都指向 X&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>歸因方向&lt;/td>
 &lt;td>設計者個人沒預見&lt;/td>
 &lt;td>工具預設 + 領域知識制度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>教讀者什麼&lt;/td>
 &lt;td>後見之明的具體規則&lt;/td>
 &lt;td>當下能用的判斷工具&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>泛化能力&lt;/td>
 &lt;td>弱（只在類似 case 適用）&lt;/td>
 &lt;td>強（可套到新情境）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩種論述讀起來都像在分析「設計缺陷」、但結論不同、可教性也不同。&lt;strong>事後諸葛論述需要結局發生、當下三軸論證在當下就能判斷&lt;/strong>——後者才是 portable 工具。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-hindsight-不適合做設計缺陷判定">為什麼 hindsight 不適合做設計缺陷判定&lt;/h2>
&lt;h3 id="hindsight-預設了結局不能在當下使用">Hindsight 預設了結局、不能在當下使用&lt;/h3>
&lt;p>事後諸葛論述「StreamController() 被當廣播用了一段時間沒事、加第二個 listener 立刻炸 → 設計缺陷」隱含一個前提：讀者知道後來加了第二個 listener。&lt;/p>
&lt;p>但設計者在當下不知道結局。如果讀者用這個論述當判斷工具、他要等到結局發生才知道「自己做錯了」——這不是判斷工具、是事後檢討。&lt;/p>
&lt;p>當下三軸論述「在零成本差異 + 強領域先驗下選了限制更高的選項 → 設計缺陷」不需要等結局——讀者拿到的是「現在面對這類選擇、跑三軸、就能判斷」的工具。&lt;/p>
&lt;h3 id="把判斷依據放在結局上會誤判類似情境">把判斷依據放在結局上、會誤判類似情境&lt;/h3>
&lt;p>「結局是『後來需要多訂閱』 → 當初該選 broadcast」這個推論、套到不同議題會推出反向結論：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>結局&lt;/th>
 &lt;th>Hindsight 推論&lt;/th>
 &lt;th>當下三軸推論&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>事先沒做 plugin、後來真的需要&lt;/td>
 &lt;td>後來需要 plugin&lt;/td>
 &lt;td>當初該建 → 過度設計&lt;/td>
 &lt;td>高成本差、屬 YAGNI 適用 → 當初不做是對的&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用 single 預設、後來只有一個訂閱者&lt;/td>
 &lt;td>沒事&lt;/td>
 &lt;td>當初選 single 是對的&lt;/td>
 &lt;td>成本對稱、領域先驗強 → 當初仍該選 broadcast、留彈性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用 sync API、後來都是 async&lt;/td>
 &lt;td>沒事或重構成本高&lt;/td>
 &lt;td>視結局而定&lt;/td>
 &lt;td>看當下三軸：簽章差異對稱、async 領域先驗強&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Hindsight 把「結局」當判斷依據 → 跟「後來剛好不需要」的 case 推出衝突結論。三軸論述用「當下選項屬性」當判斷依據 → 不依賴結局、可一致應用。&lt;/p>
&lt;h3 id="個人歸因-vs-制度歸因觀察性分歧不是必然對立">個人歸因 vs 制度歸因（觀察性分歧、不是必然對立）&lt;/h3>
&lt;p>Hindsight 論述&lt;strong>偏向&lt;/strong>個人歸因——「設計者沒預見」「沒多想」「沒考慮未來」——隱含「個人錯了」。但這不是 hindsight 的必然結果：成熟的事故報告（aviation post-mortem、medical incident report）即使用 hindsight 視角、也常透過 root cause analysis 歸因到系統 / 制度層級。&lt;/p>
&lt;p>當下三軸論述把判斷依據放在外部變數（工具預設、領域常識），歸因&lt;strong>自然偏向&lt;/strong>制度層——「工具預設選錯」「領域知識沒內化進團隊規範」。但寫作者仍可選擇用三軸論證後再歸因到個人（「三軸都指向 broadcast、但設計者選了 single」）。&lt;/p>
&lt;p>兩種論述在歸因方向上有偏差、但不是排他關係。本卡的主張不是「hindsight = 個人歸因、三軸 = 制度歸因」、而是「&lt;strong>三軸論證的歸因預設偏向制度、寫作者要明確選擇歸因層次&lt;/strong>」。&lt;/p>
&lt;p>兩種歸因引導的修補方向不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>個人歸因&lt;/strong> → 「下次更仔細」（無法行動的結論）&lt;/li>
&lt;li>&lt;strong>制度歸因&lt;/strong> → 「review checklist 加一條」「lint rule 警告」「架構規範禁掉」（可執行）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="多面向跨情境的同個結構假設性對比">多面向：跨情境的同個結構（假設性對比）&lt;/h2>
&lt;p>下面 5 個面向是&lt;strong>假設性對比&lt;/strong>——本卡用相同 framework 推到 5 個情境、但只有面向 5（技術文章寫作）有對應的真實 case（dart Stream review）。其他 4 個面向（code review / post-mortem / ADR / 個人 retro）是 framework 在不同情境的推導、未驗證真實案例中兩種論述風格的差異。讀者套用時、優先在「跟本卡 case 結構接近」的情境使用、其他面向先當「待驗證的擴張」。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>1 個 case</strong>（<a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故</a> 的 review 過程）抽出來的假說、不是經過多個獨立 case 驗證的工程原則。具體限制：</p>
<ul>
<li>「成本對稱性 / 可逆性 / 領域先驗」三軸框架借自 yagni 篇的判斷工具、不是 industry standard</li>
<li>「hindsight 偏向個人歸因 / 三軸偏向制度歸因」是觀察性分歧、不是必然對立——實際事故報告（aviation / medical incident）也常用 hindsight 同時歸因到制度</li>
<li>5 個多面向 case（code review / post-mortem / ADR / retrospective / 寫作）是假設性對比、未驗證真實案例中兩種論述的差異</li>
</ul>
<p>讀者使用本卡時、把它當「<strong>這個 blog 提倡的判定方式</strong>」、不當「驗證過的工程原則」。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p>本卡提倡的「設計缺陷」判定方式是「<strong>在當下成本對稱條件下選了限制更高的選項</strong>」、用以替代依賴結局的 hindsight 判定。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>事後諸葛論述（hindsight）</th>
          <th>當下三軸論證（current axes）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視角</td>
          <td>已知結局、回頭看</td>
          <td>不知結局、看當下選擇</td>
      </tr>
      <tr>
          <td>判斷依據</td>
          <td>後來真的需要 X、所以當初該做 X</td>
          <td>當下三軸（成本對稱性 / 可逆性 / 領域先驗）都指向 X</td>
      </tr>
      <tr>
          <td>歸因方向</td>
          <td>設計者個人沒預見</td>
          <td>工具預設 + 領域知識制度</td>
      </tr>
      <tr>
          <td>教讀者什麼</td>
          <td>後見之明的具體規則</td>
          <td>當下能用的判斷工具</td>
      </tr>
      <tr>
          <td>泛化能力</td>
          <td>弱（只在類似 case 適用）</td>
          <td>強（可套到新情境）</td>
      </tr>
  </tbody>
</table>
<p>兩種論述讀起來都像在分析「設計缺陷」、但結論不同、可教性也不同。<strong>事後諸葛論述需要結局發生、當下三軸論證在當下就能判斷</strong>——後者才是 portable 工具。</p>
<hr>
<h2 id="為什麼-hindsight-不適合做設計缺陷判定">為什麼 hindsight 不適合做設計缺陷判定</h2>
<h3 id="hindsight-預設了結局不能在當下使用">Hindsight 預設了結局、不能在當下使用</h3>
<p>事後諸葛論述「StreamController() 被當廣播用了一段時間沒事、加第二個 listener 立刻炸 → 設計缺陷」隱含一個前提：讀者知道後來加了第二個 listener。</p>
<p>但設計者在當下不知道結局。如果讀者用這個論述當判斷工具、他要等到結局發生才知道「自己做錯了」——這不是判斷工具、是事後檢討。</p>
<p>當下三軸論述「在零成本差異 + 強領域先驗下選了限制更高的選項 → 設計缺陷」不需要等結局——讀者拿到的是「現在面對這類選擇、跑三軸、就能判斷」的工具。</p>
<h3 id="把判斷依據放在結局上會誤判類似情境">把判斷依據放在結局上、會誤判類似情境</h3>
<p>「結局是『後來需要多訂閱』 → 當初該選 broadcast」這個推論、套到不同議題會推出反向結論：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>結局</th>
          <th>Hindsight 推論</th>
          <th>當下三軸推論</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事先沒做 plugin、後來真的需要</td>
          <td>後來需要 plugin</td>
          <td>當初該建 → 過度設計</td>
          <td>高成本差、屬 YAGNI 適用 → 當初不做是對的</td>
      </tr>
      <tr>
          <td>用 single 預設、後來只有一個訂閱者</td>
          <td>沒事</td>
          <td>當初選 single 是對的</td>
          <td>成本對稱、領域先驗強 → 當初仍該選 broadcast、留彈性</td>
      </tr>
      <tr>
          <td>用 sync API、後來都是 async</td>
          <td>沒事或重構成本高</td>
          <td>視結局而定</td>
          <td>看當下三軸：簽章差異對稱、async 領域先驗強</td>
      </tr>
  </tbody>
</table>
<p>Hindsight 把「結局」當判斷依據 → 跟「後來剛好不需要」的 case 推出衝突結論。三軸論述用「當下選項屬性」當判斷依據 → 不依賴結局、可一致應用。</p>
<h3 id="個人歸因-vs-制度歸因觀察性分歧不是必然對立">個人歸因 vs 制度歸因（觀察性分歧、不是必然對立）</h3>
<p>Hindsight 論述<strong>偏向</strong>個人歸因——「設計者沒預見」「沒多想」「沒考慮未來」——隱含「個人錯了」。但這不是 hindsight 的必然結果：成熟的事故報告（aviation post-mortem、medical incident report）即使用 hindsight 視角、也常透過 root cause analysis 歸因到系統 / 制度層級。</p>
<p>當下三軸論述把判斷依據放在外部變數（工具預設、領域常識），歸因<strong>自然偏向</strong>制度層——「工具預設選錯」「領域知識沒內化進團隊規範」。但寫作者仍可選擇用三軸論證後再歸因到個人（「三軸都指向 broadcast、但設計者選了 single」）。</p>
<p>兩種論述在歸因方向上有偏差、但不是排他關係。本卡的主張不是「hindsight = 個人歸因、三軸 = 制度歸因」、而是「<strong>三軸論證的歸因預設偏向制度、寫作者要明確選擇歸因層次</strong>」。</p>
<p>兩種歸因引導的修補方向不同：</p>
<ul>
<li><strong>個人歸因</strong> → 「下次更仔細」（無法行動的結論）</li>
<li><strong>制度歸因</strong> → 「review checklist 加一條」「lint rule 警告」「架構規範禁掉」（可執行）</li>
</ul>
<hr>
<h2 id="多面向跨情境的同個結構假設性對比">多面向：跨情境的同個結構（假設性對比）</h2>
<p>下面 5 個面向是<strong>假設性對比</strong>——本卡用相同 framework 推到 5 個情境、但只有面向 5（技術文章寫作）有對應的真實 case（dart Stream review）。其他 4 個面向（code review / post-mortem / ADR / 個人 retro）是 framework 在不同情境的推導、未驗證真實案例中兩種論述風格的差異。讀者套用時、優先在「跟本卡 case 結構接近」的情境使用、其他面向先當「待驗證的擴張」。</p>
<h3 id="面向-1code-review--pr-審查">面向 1：Code review / PR 審查</h3>
<p>事後諸葛 review：「你這段 code 後來在 X 條件下會炸、所以當初該寫成 Y」</p>
<p>當下三軸 review：「在當下、Y 比 X 多打 N 個字元、領域先驗強烈指向 Y、可逆性中等偏高 → Y 是當下三軸對齊的選擇」</p>
<p>差別：reviewer 不需要等到 bug 發生、就能在 PR 階段給出判斷。Hindsight review 需要等 bug、其實是「事後追究」、不是「事前防護」。</p>
<h3 id="面向-2post-mortem--事故檢討">面向 2：Post-mortem / 事故檢討</h3>
<p>事後諸葛 PM：「Stream 的單訂閱限制在第二個訂閱者出現時暴露、所以是設計缺陷」</p>
<p>當下三軸 PM：「Stream 的單訂閱選擇在當下成本對稱條件下不必要地縮小未來空間、所以是設計缺陷」</p>
<p>差別：第二種論述讓「設計缺陷」這個判定不依賴「結局已發生」、讀者學到的是 portable 判斷工具。團隊裡其他成員看到這份 PM、能套用到沒有相同結局的新議題上。</p>
<h3 id="面向-3架構決策反思adr-retrospective">面向 3：架構決策反思（ADR retrospective）</h3>
<p>事後諸葛 retro：「我們選 SOAP、後來生態系全跑去 REST、所以 SOAP 是錯的」</p>
<p>當下三軸 retro：「選 SOAP 在當時的成本對稱性是 ⋯⋯、領域先驗是 ⋯⋯ → 在那個時間點屬於合理選擇 / 屬於設計缺陷」</p>
<p>差別：能區分「真的當時就該避開」vs「環境後來變了」。前者是設計缺陷、後者是健康的演化、兩者修補方向完全不同。</p>
<h3 id="面向-4個人-retrospective">面向 4：個人 retrospective</h3>
<p>事後諸葛自我檢討：「上次我沒考慮 X、結果出事 → 下次要考慮 X」</p>
<p>當下三軸自我檢討：「上次的選擇在當下三軸論述下對齊嗎？對齊 → 不算判斷力問題、是環境變化；不對齊 → 找出當時該抓到的訊號是什麼」</p>
<p>差別：避免不必要的自我責備、改善方向更精準。「下次要考慮 X」是 case-bound、套到下一個議題（不是 X）反而沒幫助；「下次跑三軸」是 portable。</p>
<h3 id="面向-5技術文章寫作">面向 5：技術文章寫作</h3>
<p>事後諸葛寫作：「[工具] 預設為 [限制版本]、被當 [通用版本] 用了一段時間沒事、後來出問題 → 設計缺陷」</p>
<p>當下三軸寫作：「[工具] 在當下零成本差異條件下選了限制更高的選項、領域先驗指向通用版本 → 設計缺陷」</p>
<p>差別：讀者學到 portable 判斷工具、不只是「這個 case 後來怎樣」的故事。文章的長期價值（被新讀者拿去用在新議題）大幅提升。</p>
<hr>
<h2 id="識別訊號什麼時候你正在用-hindsight-論述">識別訊號：什麼時候你正在用 hindsight 論述</h2>
<h3 id="訊號-1論述需要後來最終這類時序詞">訊號 1：論述需要「後來」「最終」這類時序詞</h3>
<p>「<strong>後來</strong>加了第二個訂閱者才炸」「<strong>最終</strong>發現 plugin 系統不需要」「事故<strong>爆發後</strong>才知道」——這些時序詞標記論述依賴結局。</p>
<p>修法：把時序詞拿掉、論述還站得住嗎？站不住 → 補當下軸的論證。</p>
<h3 id="訊號-2歸因落在個人能力--預見性">訊號 2：歸因落在個人能力 / 預見性</h3>
<p>「<strong>沒預見到</strong>」「<strong>沒考慮到</strong>」「<strong>沒多想</strong>」「<strong>疏忽</strong>」——把判斷錯誤歸給個人能力。當下三軸論述會把歸因放到「工具 / 領域常識」這類外部變數。</p>
<p>修法：把「沒預見到」改成「在當下、工具預設指向 X、領域先驗指向 Y、選擇者選了 Z」——歸因從個人轉到外部結構。</p>
<h3 id="訊號-3結論無法-portable">訊號 3：結論無法 portable</h3>
<p>「下次要考慮 X」——但「X」是這次的具體議題（多訂閱）、套到不同議題（plugin 系統）反而會推出反向結論。</p>
<p>修法：結論寫成「下次跑三軸」「下次跑 checklist」——是工具、不是 case-specific 規則。</p>
<h3 id="訊號-4讀者讀完只記得-case記不住原則">訊號 4：讀者讀完只記得 case、記不住原則</h3>
<p>讀者讀完事後諸葛論述、能複述 case、但問他「下次遇到不同議題怎麼判斷」答不出來——因為原則沒被抽出來、case-bound。</p>
<p>修法：在 case 之後補一段「抽象原則」、原則用語不綁定本 case 的具體名詞。</p>
<hr>
<h2 id="何時-hindsight-論述仍然合理">何時 hindsight 論述仍然合理</h2>
<p>「設計檢討用當下三軸、不依賴 hindsight」這條原則在大多數工程檢討情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 hindsight 仍合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>真實事故 forensic 重現</td>
          <td>目的是還原時序與因果鏈、結局視角不可避免（但 root cause 仍要用三軸）</td>
      </tr>
      <tr>
          <td>學術 case study</td>
          <td>教學重點是「結局如何揭露結構」、hindsight 是 narrative 風格選擇</td>
      </tr>
      <tr>
          <td>個人日記 / 隨筆</td>
          <td>不是檢討文件、是記錄當下感受、不需要嚴格判斷工具</td>
      </tr>
      <tr>
          <td>純運氣 case（環境完全不可預測）</td>
          <td>例如外部 API 突然 deprecated、三軸無法事先抓 → 用 hindsight 描述合理</td>
      </tr>
      <tr>
          <td>教學「為什麼這個結局重要」</td>
          <td>重點是讓讀者理解結局的影響、不是教 portable 判斷工具</td>
      </tr>
  </tbody>
</table>
<p>判讀：寫之前自問「這份檢討是要產出 portable 判斷工具嗎？」——是 → 嚴格用三軸；否 → hindsight narrative 可接受。</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>hindsight 論述比三軸論述好寫（事後資料完整、結局明確）、但離 portable 意圖更遠</td>
      </tr>
      <tr>
          <td>YAGNI 三軸框架（成本對稱性 / 可逆性 / 領域先驗）</td>
          <td>本卡是三軸框架在「設計檢討寫作」面向的應用——把判斷工具寫進文章、而不是只寫結論</td>
      </tr>
      <tr>
          <td>Writing-articles 規則四：事後檢視看判讀品質</td>
          <td>本卡是規則四的進階——不只看判讀品質、還要避免結局視角偏差汙染判讀記錄</td>
      </tr>
      <tr>
          <td>Hindsight bias（認知科學）</td>
          <td>本卡把認知偏誤具體化到工程文章寫作層面、給可執行修法</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>論述用「後來」「最終」「結果」當主語</td>
          <td>改用「在當下、X 比 Y ⋯⋯」</td>
      </tr>
      <tr>
          <td>歸因落在「沒預見」「沒考慮」「沒多想」</td>
          <td>改成「工具預設選什麼 / 領域常識指向什麼 / 三軸怎麼跑」</td>
      </tr>
      <tr>
          <td>結論是「下次要記得 X」</td>
          <td>改成「下次跑三軸 / 跑 checklist」</td>
      </tr>
      <tr>
          <td>讀者複述只能複述 case</td>
          <td>補一段「抽象原則 + 套用到不同 case」</td>
      </tr>
      <tr>
          <td>文章在教「設計缺陷」但讀者要等到自己出事才懂</td>
          <td>補當下三軸論證、讓讀者在事前就能用</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：「設計缺陷」是當下成本對稱條件下選了限制更高的選項、不是事後發現需求變了。寫設計檢討時用三軸論證、避免讓判斷依賴於結局已發生——這樣讀者拿到的是 portable 判斷工具、不只是 case 故事。</p>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡的觸發是修 <a href="../../work-log/dart_stream_controller_single_vs_broadcast/">Dart StreamController：single-subscription vs broadcast 的事故實錄</a> 時的反思。</p>
<p>文章 v1 寫「StreamController() 預設為單訂閱、被當廣播用了一段時間沒事、加第二個 listener 立刻炸」——典型 hindsight 論述。</p>
<p>讀者反問：「如果只有一個人用沒問題、後來才需要多訂閱、這算設計缺陷嗎？」——這個反問正確 catch 了 hindsight 論述的薄弱處（依賴結局、不依賴當下選擇）。</p>
<p>修補後改寫：「在當下零成本差異條件下選了限制更高的選項」+ 加四格表釐清「需求演化 vs 設計缺陷」的分界——不依賴結局、用三軸論述。讀者拿到 portable 判斷工具、不只是 case 故事。</p>
<p>對應本卡：<strong>寫設計檢討時、第一版常常用 hindsight（因為事故已發生、視角自然帶結局）——要刻意 review 改成當下三軸、才會產出 portable 工具</strong>。這個 review 不會自動發生、需要 multi-pass review 流程在「規則三 / 規則四對齊」輪刻意檢查。</p>
]]></content:encoded></item><item><title>口語化修辭在判斷工具型段落會稀釋技術精度</title><link>https://tarrragon.github.io/blog/report/colloquial-rhetoric-erodes-technical-precision/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/colloquial-rhetoric-erodes-technical-precision/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>1 個 case&lt;/strong>（&lt;a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故的 review&lt;/a>）抽出來的觀察。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Keyword bank 是 starting set、不是 exhaustive list&lt;/strong>：列出的口語詞（一輩子 / 碰巧 / 撞牆 / 啊原來）都是這次 dart 篇出現過的、不代表「口語修辭的完整詞庫」。新類型出現時要持續擴充&lt;/li>
&lt;li>&lt;strong>「精度 vs 可讀性」是情境化取捨、不是 zero-sum&lt;/strong>：本卡聚焦「判斷工具型段落」（讀者要從中拿到 portable 工具的論述）、在這類段落精度優先；但 hook / 引言 / narrative 段落口語反而幫讀者進入論述、不適用本卡&lt;/li>
&lt;li>&lt;strong>Self-case 的修補有效性未獨立驗證&lt;/strong>：dart 篇修補後讀起來更精準、但「精準度跟讀者實際理解之間的相關性」沒做使用者測試&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、先判斷段落是否屬於「判斷工具型」——是 → 套用；否 → 評估口語跟可讀性的取捨。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>技術文章的「&lt;strong>判斷工具型段落&lt;/strong>」（讀者用來判斷自己 case 的論述）裡、用詞選擇要能對應到具體技術屬性。口語修辭（「一輩子」「碰巧」「立刻撞牆」「沒事」）讀起來流暢、但在這類段落會稀釋精度——精度標準是「&lt;strong>用詞能不能反推到具體機制 / 條件 / 契約&lt;/strong>」、口語修辭多半不能、寫了等於把判斷工具退化成感性印象。&lt;/p>
&lt;p>下面三個層次的稀釋是觀察到的 pattern——不是窮舉、實際違規模式可能更多：&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>「一輩子只能 X」&lt;/td>
 &lt;td>時間性誇張&lt;/td>
 &lt;td>永遠這樣（無範圍感）&lt;/td>
 &lt;td>生命週期內最多承載 1 次&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>視覺意象、不知為什麼&lt;/td>
 &lt;td>違反型別契約、執行期 throw&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>知道很糟、不知糟在哪&lt;/td>
 &lt;td>違反契約導致 X 行為&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「啊原來 X」&lt;/td>
 &lt;td>結局視角的驚訝句&lt;/td>
 &lt;td>事後恍然大悟感&lt;/td>
 &lt;td>在 Y 條件下 X 才暴露&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="三個稀釋層次的具體-case">三個稀釋層次的具體 case&lt;/h2>
&lt;h3 id="層次-1時間性誇張">層次 1：時間性誇張&lt;/h3>
&lt;p>&lt;strong>口語版&lt;/strong>：「stream 一輩子只能被 listen 一次」&lt;/p>
&lt;p>&lt;strong>問題&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>「一輩子」沒有對應的技術概念——是「整個 process lifetime」？「stream object lifetime」？「listener cancel 之後也算嗎」？&lt;/li>
&lt;li>讀者要靠猜測決定範圍&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>精度版&lt;/strong>：「stream 在整個生命週期內只允許被 listen 一次（&lt;code>StreamController&lt;/code> object lifetime；&lt;code>cancel()&lt;/code> 之後再 &lt;code>listen()&lt;/code> 仍違反契約、要重建 &lt;code>StreamController&lt;/code>）」&lt;/p>
&lt;p>差別：精度版讓讀者能反推「我這個 case 屬於哪個範圍、會不會踩到」、口語版讀者只能感受「應該很嚴格」。&lt;/p>
&lt;h3 id="層次-2因果模糊">層次 2：因果模糊&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;/ul>
&lt;p>&lt;strong>精度版&lt;/strong>：「訂閱者單一時、限制處於沉默狀態」&lt;/p>
&lt;p>差別：精度版描述「為什麼沒影響」的具體條件、口語版只描述「結果」。讀者能用精度版去判斷自己的 case 何時會觸發、不能用口語版做同樣的判斷。&lt;/p>
&lt;h3 id="層次-3結局描述代替契約描述">層次 3：結局描述代替契約描述&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>套到不同 case（如「使用 cancelled stream」「使用 closed controller」）會推不出對應的修法&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>精度版&lt;/strong>：「新加第二個訂閱者直接違反契約、執行期 throw」&lt;/p>
&lt;p>差別：精度版指明「違反的是什麼（契約）」「在哪個時間點（執行期）」「具體後果（throw）」、讀者能 generalize 到其他違反契約的情境。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼口語修辭會出現">為什麼口語修辭會出現&lt;/h2>
&lt;p>技術寫作中口語修辭的出現有三類典型來源：&lt;/p>
&lt;h3 id="來源-1寫的當下感性敘事比技術敘事好寫">來源 1：寫的當下感性敘事比技術敘事好寫&lt;/h3>
&lt;p>「一輩子只能 listen 一次」比「整個 lifecycle 只允許被 listen 一次」少 5 個字、寫起來順、語氣強烈。但精度也差距 5 倍。寫作便利度高的版本通常離精度遠（&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a> 的同骨——寫得順 ≠ 寫得對）。&lt;/p>
&lt;h3 id="來源-2對讀者直覺感受的妥協">來源 2：對讀者「直覺感受」的妥協&lt;/h3>
&lt;p>「碰巧能用」這類詞讓讀者「立刻有共鳴」、感覺像在讀故事。但技術文章的讀者來這裡找的是判斷工具、不是故事——共鳴感換來精度損失、是錯誤的取捨。&lt;/p>
&lt;h3 id="來源-3誤把事故當下的反應當成論述用詞">來源 3：誤把「事故當下的反應」當成「論述用詞」&lt;/h3>
&lt;p>事故當下工程師的內心 OS 確實是「啊原來這個 mutation 路徑沒帶」「進入頁面就炸」——這些是當下的口語反應。寫成正式論述時要把這些反應翻譯回技術描述、保留意圖（驚訝 / 嚴重程度）但不保留口語形式。&lt;/p>
&lt;hr>
&lt;h2 id="識別訊號什麼時候你在用口語修辭">識別訊號：什麼時候你在用口語修辭&lt;/h2>
&lt;h3 id="訊號-1時間--範圍誇張詞">訊號 1：時間 / 範圍誇張詞&lt;/h3>
&lt;p>「一輩子」「永遠」「整個炸了」「全部 GG」「直接死」——這類詞沒有對應的技術範圍、用了讀者要靠猜測。&lt;/p>
&lt;p>修法：問「具體是哪個範圍 / 哪個生命週期 / 哪個 scope？」、把答案寫進去取代誇張詞。&lt;/p>
&lt;h3 id="訊號-2運氣--巧合語氣">訊號 2：運氣 / 巧合語氣&lt;/h3>
&lt;p>「碰巧」「剛好」「湊巧」「運氣好」——這類詞遮蔽了背後的機制。技術系統的行為幾乎都有可解釋的條件、不是運氣。&lt;/p>
&lt;p>修法：問「為什麼當下沒發生？是哪個條件沒滿足？」、把條件寫出來取代運氣語氣。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>1 個 case</strong>（<a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故的 review</a>）抽出來的觀察。具體限制：</p>
<ul>
<li><strong>Keyword bank 是 starting set、不是 exhaustive list</strong>：列出的口語詞（一輩子 / 碰巧 / 撞牆 / 啊原來）都是這次 dart 篇出現過的、不代表「口語修辭的完整詞庫」。新類型出現時要持續擴充</li>
<li><strong>「精度 vs 可讀性」是情境化取捨、不是 zero-sum</strong>：本卡聚焦「判斷工具型段落」（讀者要從中拿到 portable 工具的論述）、在這類段落精度優先；但 hook / 引言 / narrative 段落口語反而幫讀者進入論述、不適用本卡</li>
<li><strong>Self-case 的修補有效性未獨立驗證</strong>：dart 篇修補後讀起來更精準、但「精準度跟讀者實際理解之間的相關性」沒做使用者測試</li>
</ul>
<p>讀者使用本卡時、先判斷段落是否屬於「判斷工具型」——是 → 套用；否 → 評估口語跟可讀性的取捨。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p>技術文章的「<strong>判斷工具型段落</strong>」（讀者用來判斷自己 case 的論述）裡、用詞選擇要能對應到具體技術屬性。口語修辭（「一輩子」「碰巧」「立刻撞牆」「沒事」）讀起來流暢、但在這類段落會稀釋精度——精度標準是「<strong>用詞能不能反推到具體機制 / 條件 / 契約</strong>」、口語修辭多半不能、寫了等於把判斷工具退化成感性印象。</p>
<p>下面三個層次的稀釋是觀察到的 pattern——不是窮舉、實際違規模式可能更多：</p>
<table>
  <thead>
      <tr>
          <th>口語修辭</th>
          <th>稀釋的層次</th>
          <th>讀者拿到的訊息</th>
          <th>真實技術屬性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「一輩子只能 X」</td>
          <td>時間性誇張</td>
          <td>永遠這樣（無範圍感）</td>
          <td>生命週期內最多承載 1 次</td>
      </tr>
      <tr>
          <td>「碰巧能用」</td>
          <td>因果模糊</td>
          <td>運氣使然</td>
          <td>觸發條件不滿足、限制處於沉默狀態</td>
      </tr>
      <tr>
          <td>「立刻撞牆」</td>
          <td>結局描述代替契約描述</td>
          <td>視覺意象、不知為什麼</td>
          <td>違反型別契約、執行期 throw</td>
      </tr>
      <tr>
          <td>「沒事」</td>
          <td>缺失條件描述</td>
          <td>沒問題</td>
          <td>限制存在、但沒有可見影響</td>
      </tr>
      <tr>
          <td>「整個炸了」</td>
          <td>嚴重度誇張</td>
          <td>知道很糟、不知糟在哪</td>
          <td>違反契約導致 X 行為</td>
      </tr>
      <tr>
          <td>「啊原來 X」</td>
          <td>結局視角的驚訝句</td>
          <td>事後恍然大悟感</td>
          <td>在 Y 條件下 X 才暴露</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="三個稀釋層次的具體-case">三個稀釋層次的具體 case</h2>
<h3 id="層次-1時間性誇張">層次 1：時間性誇張</h3>
<p><strong>口語版</strong>：「stream 一輩子只能被 listen 一次」</p>
<p><strong>問題</strong>：</p>
<ul>
<li>「一輩子」沒有對應的技術概念——是「整個 process lifetime」？「stream object lifetime」？「listener cancel 之後也算嗎」？</li>
<li>讀者要靠猜測決定範圍</li>
</ul>
<p><strong>精度版</strong>：「stream 在整個生命週期內只允許被 listen 一次（<code>StreamController</code> object lifetime；<code>cancel()</code> 之後再 <code>listen()</code> 仍違反契約、要重建 <code>StreamController</code>）」</p>
<p>差別：精度版讓讀者能反推「我這個 case 屬於哪個範圍、會不會踩到」、口語版讀者只能感受「應該很嚴格」。</p>
<h3 id="層次-2因果模糊">層次 2：因果模糊</h3>
<p><strong>口語版</strong>：「碰巧能用掩蓋了設計缺陷」</p>
<p><strong>問題</strong>：</p>
<ul>
<li>「碰巧」暗示運氣使然、機制不可解釋</li>
<li>但實際上「能用」有明確的機制：訂閱者數量沒達到觸發違反契約的條件</li>
<li>用「碰巧」會讓讀者學到「這是運氣問題」、不學到「這是條件未滿足的可預測現象」</li>
</ul>
<p><strong>精度版</strong>：「訂閱者單一時、限制處於沉默狀態」</p>
<p>差別：精度版描述「為什麼沒影響」的具體條件、口語版只描述「結果」。讀者能用精度版去判斷自己的 case 何時會觸發、不能用口語版做同樣的判斷。</p>
<h3 id="層次-3結局描述代替契約描述">層次 3：結局描述代替契約描述</h3>
<p><strong>口語版</strong>：「新加第二個訂閱者就立刻撞牆」</p>
<p><strong>問題</strong>：</p>
<ul>
<li>「撞牆」是視覺意象、不是技術描述</li>
<li>讀者拿到的是「發生了不好的事」、不是「為什麼發生」「在哪一層發生」</li>
<li>套到不同 case（如「使用 cancelled stream」「使用 closed controller」）會推不出對應的修法</li>
</ul>
<p><strong>精度版</strong>：「新加第二個訂閱者直接違反契約、執行期 throw」</p>
<p>差別：精度版指明「違反的是什麼（契約）」「在哪個時間點（執行期）」「具體後果（throw）」、讀者能 generalize 到其他違反契約的情境。</p>
<hr>
<h2 id="為什麼口語修辭會出現">為什麼口語修辭會出現</h2>
<p>技術寫作中口語修辭的出現有三類典型來源：</p>
<h3 id="來源-1寫的當下感性敘事比技術敘事好寫">來源 1：寫的當下感性敘事比技術敘事好寫</h3>
<p>「一輩子只能 listen 一次」比「整個 lifecycle 只允許被 listen 一次」少 5 個字、寫起來順、語氣強烈。但精度也差距 5 倍。寫作便利度高的版本通常離精度遠（<a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a> 的同骨——寫得順 ≠ 寫得對）。</p>
<h3 id="來源-2對讀者直覺感受的妥協">來源 2：對讀者「直覺感受」的妥協</h3>
<p>「碰巧能用」這類詞讓讀者「立刻有共鳴」、感覺像在讀故事。但技術文章的讀者來這裡找的是判斷工具、不是故事——共鳴感換來精度損失、是錯誤的取捨。</p>
<h3 id="來源-3誤把事故當下的反應當成論述用詞">來源 3：誤把「事故當下的反應」當成「論述用詞」</h3>
<p>事故當下工程師的內心 OS 確實是「啊原來這個 mutation 路徑沒帶」「進入頁面就炸」——這些是當下的口語反應。寫成正式論述時要把這些反應翻譯回技術描述、保留意圖（驚訝 / 嚴重程度）但不保留口語形式。</p>
<hr>
<h2 id="識別訊號什麼時候你在用口語修辭">識別訊號：什麼時候你在用口語修辭</h2>
<h3 id="訊號-1時間--範圍誇張詞">訊號 1：時間 / 範圍誇張詞</h3>
<p>「一輩子」「永遠」「整個炸了」「全部 GG」「直接死」——這類詞沒有對應的技術範圍、用了讀者要靠猜測。</p>
<p>修法：問「具體是哪個範圍 / 哪個生命週期 / 哪個 scope？」、把答案寫進去取代誇張詞。</p>
<h3 id="訊號-2運氣--巧合語氣">訊號 2：運氣 / 巧合語氣</h3>
<p>「碰巧」「剛好」「湊巧」「運氣好」——這類詞遮蔽了背後的機制。技術系統的行為幾乎都有可解釋的條件、不是運氣。</p>
<p>修法：問「為什麼當下沒發生？是哪個條件沒滿足？」、把條件寫出來取代運氣語氣。</p>
<h3 id="訊號-3視覺意象--動作比喻">訊號 3：視覺意象 / 動作比喻</h3>
<p>「撞牆」「炸了」「鎖死」「卡住」「當機」（口語的，不是技術術語的 hang/freeze）——這些是事件的視覺描述、不是契約 / 機制描述。</p>
<p>修法：問「在哪一層、違反了什麼、執行期表現是什麼？」、把答案寫出來取代視覺比喻。</p>
<h3 id="訊號-4結局視角的驚訝句">訊號 4：結局視角的驚訝句</h3>
<p>「啊原來」「結果發現」「最後才知道」——這些把讀者帶到「事故已發生、回頭看」的視角、依賴 hindsight。</p>
<p>修法：問「在事前、什麼條件成立時這個 X 才會出現？」、改成事前可判斷的描述。詳細處理見 <a href="../design-flaw-by-current-axes-not-hindsight/">#110 設計檢討用當下三軸論證、不依賴 hindsight</a>。</p>
<h3 id="訊號-5沒事沒問題當成段落結論">訊號 5：「沒事」「沒問題」當成段落結論</h3>
<p>「運作正常」「沒事」「沒問題」「一切看起來都好」——這類詞代替了「在哪些條件下沒可見影響」的具體描述、暗示「不需要進一步審視」。</p>
<p>修法：問「沒影響是因為什麼條件沒觸發？換條件會怎樣？」、把條件寫出來取代「沒事」。</p>
<h3 id="訊號-6下次-x-時做-y結尾段">訊號 6：「下次 X 時、做 Y」結尾段</h3>
<p>「下次看到 X 時、先問 Y」「下次寫 Z 時、想 W」「下次面對 V 時、停下來」——這類段落想給讀者實踐建議、但有兩個結構問題：</p>
<ol>
<li><strong>前綴是 wrapper、不增加資訊</strong>：讀者來讀這段的脈絡通常已經是「正在處理 X」、「下次看到 X 時」對讀者是廢話、把重點推到後面（違反規則五「最重要的話優先說」）</li>
<li><strong>預設讀者會主動回憶</strong>：「下次 X 時做 Y」假設讀者下次會自動想起這條建議、實際上讀者多半不會主動 recall——應該寫成「<strong>判斷工具</strong>」而不是「<strong>回憶指令</strong>」</li>
</ol>
<p>修法：把 wrapper 拿掉、把建議改寫成判斷工具的形式：</p>
<table>
  <thead>
      <tr>
          <th>修補前</th>
          <th>修補後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>下次看到 <code>Bad state</code> 時、不要先想「我寫錯了」</td>
          <td>根因落在 stream 定義端的型別契約、不在訂閱端</td>
      </tr>
      <tr>
          <td>下次寫 test 時、想「這份 file 是唯一 doc 嗎」</td>
          <td>把「這份 file 是模組唯一 doc」當命名的質量門檻</td>
      </tr>
      <tr>
          <td>下次看到自己寫三行 doc、停下來想能不能變型別</td>
          <td>寫到「三行 doc 解釋輸入範圍」這個訊號時、自問能不能變型別簽章</td>
      </tr>
  </tbody>
</table>
<p>差別：修補前是「下次 X 時、做 Y」的 hortative 命令；修補後是「<strong>遇到 X 訊號時、用 Y 工具判斷</strong>」的 reactive 工具。前者依賴讀者主動 recall、後者由訊號觸發判斷。</p>
<p>注意 ≠「描述讀者的具體情境」：</p>
<blockquote>
<p>「讀者讀完事後諸葛論述、能複述 case、但問他<strong>下次遇到不同議題怎麼判斷</strong>答不出來」</p></blockquote>
<p>這個「下次遇到 X」是描述讀者的測驗情境（檢驗論述是否 portable）、不是給讀者的指令——保留合理。</p>
<hr>
<h2 id="何時口語修辭仍然合理">何時口語修辭仍然合理</h2>
<p>「精度優先」這條原則在大多數技術論述情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼口語仍合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對話 / 訊息 / Slack 討論</td>
          <td>即時溝通、共識在場、口語反而更快建立 mutual understanding</td>
      </tr>
      <tr>
          <td>個人筆記 / scratchpad</td>
          <td>寫給自己看、知道自己當下在想什麼、不需要對他人精度</td>
      </tr>
      <tr>
          <td>故事性引言 / hook 段</td>
          <td>文章開頭吸引讀者、口語可暫時用、但很快要切回精度敘事</td>
      </tr>
      <tr>
          <td>引用當事人的反應 / 直接 quote</td>
          <td>「事故當下工程師說『啊我哪裡寫錯了』」是引用、保留口語反映真實心境</td>
      </tr>
      <tr>
          <td>反例段落（教讀者「不要這樣寫」）</td>
          <td>故意用口語版讓讀者看到差別、然後切到精度版</td>
      </tr>
  </tbody>
</table>
<p>判讀：寫之前自問「讀者拿這段去做技術判斷時、口語修辭會不會讓判斷模糊？」——會 → 翻成精度版；不會 → 口語可接受。</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>口語修辭比精度敘事好寫——但精度才是讀者要的工具、便利 vs 對齊的同骨展現</td>
      </tr>
      <tr>
          <td><a href="../design-flaw-by-current-axes-not-hindsight/">#110 設計檢討用當下三軸論證、不依賴 hindsight</a></td>
          <td>「啊原來」「碰巧」這類詞天生帶 hindsight 視角、本卡是 #110 在「字句層級」的延伸</td>
      </tr>
      <tr>
          <td>Compositional-writing 規則一：階段分層（觀察 → 判讀 → 策略 → 執行）</td>
          <td>口語修辭多半混淆四階段——「立刻撞牆」混了觀察跟判讀、要拆開重寫</td>
      </tr>
      <tr>
          <td>Compositional-writing 規則二：商業邏輯先於 CASE</td>
          <td>「碰巧能用」隱藏商業邏輯（為什麼能用）、本卡要求把商業邏輯顯式寫出來</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>用了「一輩子」「永遠」「整個」這類絕對詞</td>
          <td>換成具體生命週期 / scope</td>
      </tr>
      <tr>
          <td>用了「碰巧」「剛好」這類運氣語氣</td>
          <td>找出背後的機制條件、寫出條件</td>
      </tr>
      <tr>
          <td>用了「撞牆」「炸了」這類視覺意象</td>
          <td>換成「違反 X 契約 / 在 Y 時刻 throw / 觸發 Z error」</td>
      </tr>
      <tr>
          <td>用了「啊原來」「結果發現」</td>
          <td>改成事前可判斷的條件描述</td>
      </tr>
      <tr>
          <td>用了「沒事」當段落結論</td>
          <td>補「在 X 條件下沒影響、換 Y 條件會怎樣」</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：技術文章的用詞要能反推到具體機制 / 條件 / 契約。口語修辭的便利在於寫得順、代價是讀者拿不到判斷工具。寫完每段後跑一次「這個詞對應什麼具體技術屬性？」自問、答不出來的詞就要翻譯。</p>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡的觸發是修 <a href="../../work-log/dart_stream_controller_single_vs_broadcast/">Dart StreamController：single-subscription vs broadcast 的事故實錄</a> 的第二輪審查。</p>
<p>讀者指出該文有四處口語化問題：「stream 一輩子只能被 listen 一次」「進入頁面就炸」「現在只要有人訂閱、把它記錄下來，UI 就能用」「碰巧能用掩蓋了設計缺陷」。修補時的觀察是這些不是孤立問題、是「<strong>口語修辭代替技術描述</strong>」這個共通 frame 在不同句子的展現。</p>
<p>修補後對比：</p>
<table>
  <thead>
      <tr>
          <th>修補前</th>
          <th>修補後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>stream 一輩子只能被 listen 一次</td>
          <td>整個生命週期內只允許被 listen 一次（<code>StreamController</code> object lifetime）</td>
      </tr>
      <tr>
          <td>進入頁面就炸</td>
          <td>第二個訂閱者觸發底層限制</td>
      </tr>
      <tr>
          <td>立刻撞牆</td>
          <td>直接違反契約、執行期 throw</td>
      </tr>
      <tr>
          <td>碰巧能用</td>
          <td>限制處於沉默狀態（觸發條件不滿足）</td>
      </tr>
      <tr>
          <td>啊原來這個 mutation 路徑沒帶</td>
          <td>暴露出某些 mutation 路徑沒填寫該欄位</td>
      </tr>
  </tbody>
</table>
<p>每一組修補都把「口語視覺意象」翻譯回「技術屬性 + 條件 + 契約」。讀者拿到的是可反推的判斷工具、不是感性印象。</p>
<p>對應本卡：<strong>口語修辭的稀釋是字句層級的、容易在第一輪寫作後被忽略——multi-pass review 要在輪 4「Grep-ability / 命名 / 術語」加掃這個 frame、用 grep 找口語詞庫</strong>。</p>
]]></content:encoded></item><item><title>地區用語對齊：寫作前先確定讀者的中文語料</title><link>https://tarrragon.github.io/blog/report/regional-terminology-alignment/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/regional-terminology-alignment/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>1 個 case&lt;/strong>（&lt;a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故的 review&lt;/a> 中讀者點出「副屏」是中國用語）抽出來的觀察。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>詞庫表是分布偏差、不是絕對對立&lt;/strong>：「函數 vs 函式」「介面 vs 接口」這類詞在兩岸都有人用、只是分布偏好不同。本卡的「漂移代價」是觀察性敘述、不是嚴格的語言學分類&lt;/li>
&lt;li>&lt;strong>部分詞的判定可能不準&lt;/strong>：例如「文件」在台灣 IT 圈也常用來指 file（Windows 檔案總管的「文件」資料夾用了二十年）、「視頻」近年也滲透台灣年輕人語料——本卡列為「中國用語」是偏向 IT 業界傳統用法的 heuristic、不適用所有讀者群&lt;/li>
&lt;li>&lt;strong>沒有量化資料支撐「對映成本」&lt;/strong>：本卡描述「累積性的微秒級對映成本」是直覺論述、未做使用者測試&lt;/li>
&lt;li>&lt;strong>預設台灣讀者&lt;/strong>：本卡用「對台灣讀者寫作」當預設、實際讀者群可能跨地區、預設單一地區可能 over-narrow&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、把詞庫表當「&lt;strong>這個 blog 為台灣 IT 工程師讀者預設的用詞偏好&lt;/strong>」、不當「兩岸標準語料對照表」。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>中文技術寫作的用詞精度受&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>螢幕 / 副螢幕&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>「文件」在繁中既可指 document（公文書）、也可指 file（檔案總管的「文件」資料夾）、用法分歧&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>質量&lt;/td>
 &lt;td>「質量」在繁中是 mass / weight、技術文章誤用嚴重&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>函數 / 變量&lt;/td>
 &lt;td>「函數」「函式」兩岸都通用、但業界（尤其 Dart / Swift / TypeScript 社群）偏好「函式」&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>接口&lt;/td>
 &lt;td>「介面」在繁中明確指 interface&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內存 / 記憶體&lt;/td>
 &lt;td>記憶體&lt;/td>
 &lt;td>內存&lt;/td>
 &lt;td>「內存」在繁中是「內部存放」、不是 memory&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>技術文章的精度標準是「&lt;strong>讀者用自己的語料能正確 parse 你的句子&lt;/strong>」。用錯地區用語、輕則增加讀者的對映成本（雖然可讀但不順）、重則造成語意誤判（如「文件」當 file 用、繁中讀者預期是 document）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼地區用語會漂移">為什麼地區用語會漂移&lt;/h2>
&lt;h3 id="來源-1訓練語料--參考材料來源">來源 1：訓練語料 / 參考材料來源&lt;/h3>
&lt;p>技術寫作者常從中國技術社群、AI 工具（早期 LLM 的中文訓練資料以簡中為主）、open source 文件抓參考材料。複製時容易帶進原語料的用詞。&lt;/p>
&lt;h3 id="來源-2技術圈的-stack-overflow-效應">來源 2：技術圈的 Stack Overflow 效應&lt;/h3>
&lt;p>部分技術術語在簡中社群有大量內容、繁中相對少。寫作時搜尋資料、看到簡中版本的解釋、不自覺套用對方用詞。&lt;/p>
&lt;h3 id="來源-3讀得懂就好的低估">來源 3：「讀得懂就好」的低估&lt;/h3>
&lt;p>「副屏 vs 副螢幕、讀者應該都看得懂」——這個假設在「個別詞」層級成立、但在「整篇文章」層級會累積：每個詞累積一次對映成本、整篇加起來幾十個詞、讀者的認知負擔會增加。&lt;/p>
&lt;h3 id="來源-4跨地區團隊的混合語料">來源 4：跨地區團隊的混合語料&lt;/h3>
&lt;p>跨地區團隊（台灣 + 中國 + 香港 + 馬來西亞）的工程師常混用兩邊用詞。內部溝通沒問題、但寫成對外文章時要選定一個地區的語料對齊。&lt;/p>
&lt;hr>
&lt;h2 id="識別訊號什麼時候你在用錯地區用語">識別訊號：什麼時候你在用錯地區用語&lt;/h2>
&lt;h3 id="訊號-1技術術語有兩個常見譯法">訊號 1：技術術語有兩個常見譯法&lt;/h3>
&lt;p>「函式 vs 函數」「介面 vs 接口」「預設 vs 默認」「記憶體 vs 內存」——這類詞兩邊都有用、但分布偏向不同地區。&lt;/p>
&lt;p>修法：選定讀者地區的版本、全篇統一。對台灣讀者寫作時用「函式 / 介面 / 預設 / 記憶體」。&lt;/p>
&lt;h3 id="訊號-2硬體--業務名詞用了單字版本">訊號 2：硬體 / 業務名詞用了單字版本&lt;/h3>
&lt;p>「屏」「網盤」「U 盤」「光盤」——簡中習慣用單字、繁中習慣用兩字以上。&lt;/p>
&lt;p>修法：「螢幕 / 雲端硬碟 / USB 隨身碟 / 光碟」。&lt;/p>
&lt;h3 id="訊號-3質量當品質用">訊號 3:「質量」當品質用&lt;/h3>
&lt;p>「程式碼質量」「服務質量」——繁中「質量」是 mass、用「品質」較精確。&lt;/p>
&lt;p>修法：technical quality → 品質；mass / weight → 質量。&lt;/p>
&lt;h3 id="訊號-4視頻--視屏用在繁中文章">訊號 4：「視頻 / 視屏」用在繁中文章&lt;/h3>
&lt;p>「教學視頻」「示範視屏」——繁中傳統用「影片」、但「視頻」近年滲透。對 IT 工程師讀者寫作時、用「影片」更貼合主流偏好。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>1 個 case</strong>（<a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故的 review</a> 中讀者點出「副屏」是中國用語）抽出來的觀察。具體限制：</p>
<ul>
<li><strong>詞庫表是分布偏差、不是絕對對立</strong>：「函數 vs 函式」「介面 vs 接口」這類詞在兩岸都有人用、只是分布偏好不同。本卡的「漂移代價」是觀察性敘述、不是嚴格的語言學分類</li>
<li><strong>部分詞的判定可能不準</strong>：例如「文件」在台灣 IT 圈也常用來指 file（Windows 檔案總管的「文件」資料夾用了二十年）、「視頻」近年也滲透台灣年輕人語料——本卡列為「中國用語」是偏向 IT 業界傳統用法的 heuristic、不適用所有讀者群</li>
<li><strong>沒有量化資料支撐「對映成本」</strong>：本卡描述「累積性的微秒級對映成本」是直覺論述、未做使用者測試</li>
<li><strong>預設台灣讀者</strong>：本卡用「對台灣讀者寫作」當預設、實際讀者群可能跨地區、預設單一地區可能 over-narrow</li>
</ul>
<p>讀者使用本卡時、把詞庫表當「<strong>這個 blog 為台灣 IT 工程師讀者預設的用詞偏好</strong>」、不當「兩岸標準語料對照表」。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p>中文技術寫作的用詞精度受<strong>讀者的地區語料</strong>影響——同一個概念在繁中跟簡中可能用完全不同的詞、或用同一個詞但語意稍微偏移。寫作前先確定讀者的中文語料、避免用對方語料中不存在或意思偏移的詞。</p>
<table>
  <thead>
      <tr>
          <th>概念類別</th>
          <th>繁中（台灣）</th>
          <th>簡中（中國）</th>
          <th>漂移代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>硬體 / 顯示</td>
          <td>螢幕 / 副螢幕</td>
          <td>屏 / 副屏</td>
          <td>讀者要多花一次對映、且暗示寫作者地區</td>
      </tr>
      <tr>
          <td>檔案系統</td>
          <td>檔案 / 資料夾</td>
          <td>文件 / 文件夾</td>
          <td>「文件」在繁中既可指 document（公文書）、也可指 file（檔案總管的「文件」資料夾）、用法分歧</td>
      </tr>
      <tr>
          <td>預設值</td>
          <td>預設</td>
          <td>默認</td>
          <td>「默認」在繁中是「沉默接受」、語意不同</td>
      </tr>
      <tr>
          <td>品質 / 質量</td>
          <td>品質</td>
          <td>質量</td>
          <td>「質量」在繁中是 mass / weight、技術文章誤用嚴重</td>
      </tr>
      <tr>
          <td>視訊 / 影片</td>
          <td>影片 / 視訊</td>
          <td>視頻 / 視屏</td>
          <td>「視頻」在繁中傳統較少用、但近年滲透增加</td>
      </tr>
      <tr>
          <td>程式設計術語</td>
          <td>函式 / 變數</td>
          <td>函數 / 變量</td>
          <td>「函數」「函式」兩岸都通用、但業界（尤其 Dart / Swift / TypeScript 社群）偏好「函式」</td>
      </tr>
      <tr>
          <td>業務術語</td>
          <td>訂單 / 結帳</td>
          <td>訂單 / 收銀</td>
          <td>「收銀」兩邊通用、但「結帳」在繁中更常用</td>
      </tr>
      <tr>
          <td>介面 / 接口</td>
          <td>介面</td>
          <td>接口</td>
          <td>「介面」在繁中明確指 interface</td>
      </tr>
      <tr>
          <td>內存 / 記憶體</td>
          <td>記憶體</td>
          <td>內存</td>
          <td>「內存」在繁中是「內部存放」、不是 memory</td>
      </tr>
  </tbody>
</table>
<p>技術文章的精度標準是「<strong>讀者用自己的語料能正確 parse 你的句子</strong>」。用錯地區用語、輕則增加讀者的對映成本（雖然可讀但不順）、重則造成語意誤判（如「文件」當 file 用、繁中讀者預期是 document）。</p>
<hr>
<h2 id="為什麼地區用語會漂移">為什麼地區用語會漂移</h2>
<h3 id="來源-1訓練語料--參考材料來源">來源 1：訓練語料 / 參考材料來源</h3>
<p>技術寫作者常從中國技術社群、AI 工具（早期 LLM 的中文訓練資料以簡中為主）、open source 文件抓參考材料。複製時容易帶進原語料的用詞。</p>
<h3 id="來源-2技術圈的-stack-overflow-效應">來源 2：技術圈的 Stack Overflow 效應</h3>
<p>部分技術術語在簡中社群有大量內容、繁中相對少。寫作時搜尋資料、看到簡中版本的解釋、不自覺套用對方用詞。</p>
<h3 id="來源-3讀得懂就好的低估">來源 3：「讀得懂就好」的低估</h3>
<p>「副屏 vs 副螢幕、讀者應該都看得懂」——這個假設在「個別詞」層級成立、但在「整篇文章」層級會累積：每個詞累積一次對映成本、整篇加起來幾十個詞、讀者的認知負擔會增加。</p>
<h3 id="來源-4跨地區團隊的混合語料">來源 4：跨地區團隊的混合語料</h3>
<p>跨地區團隊（台灣 + 中國 + 香港 + 馬來西亞）的工程師常混用兩邊用詞。內部溝通沒問題、但寫成對外文章時要選定一個地區的語料對齊。</p>
<hr>
<h2 id="識別訊號什麼時候你在用錯地區用語">識別訊號：什麼時候你在用錯地區用語</h2>
<h3 id="訊號-1技術術語有兩個常見譯法">訊號 1：技術術語有兩個常見譯法</h3>
<p>「函式 vs 函數」「介面 vs 接口」「預設 vs 默認」「記憶體 vs 內存」——這類詞兩邊都有用、但分布偏向不同地區。</p>
<p>修法：選定讀者地區的版本、全篇統一。對台灣讀者寫作時用「函式 / 介面 / 預設 / 記憶體」。</p>
<h3 id="訊號-2硬體--業務名詞用了單字版本">訊號 2：硬體 / 業務名詞用了單字版本</h3>
<p>「屏」「網盤」「U 盤」「光盤」——簡中習慣用單字、繁中習慣用兩字以上。</p>
<p>修法：「螢幕 / 雲端硬碟 / USB 隨身碟 / 光碟」。</p>
<h3 id="訊號-3質量當品質用">訊號 3:「質量」當品質用</h3>
<p>「程式碼質量」「服務質量」——繁中「質量」是 mass、用「品質」較精確。</p>
<p>修法：technical quality → 品質；mass / weight → 質量。</p>
<h3 id="訊號-4視頻--視屏用在繁中文章">訊號 4：「視頻 / 視屏」用在繁中文章</h3>
<p>「教學視頻」「示範視屏」——繁中傳統用「影片」、但「視頻」近年滲透。對 IT 工程師讀者寫作時、用「影片」更貼合主流偏好。</p>
<h3 id="訊號-5文件當-file-用的歧義">訊號 5：「文件」當 file 用的歧義</h3>
<p>「打開這個文件」「保存文件」——繁中的「文件」用法分歧（IT 業界常用「檔案」指 file、但日常 / Windows 檔案總管也用「文件」指資料夾）。寫作時若指 file、用「檔案」更明確。</p>
<p>修法：file → 檔案（避免歧義）；document（公文 / 報告）→ 文件。</p>
<hr>
<h2 id="何時混用--不嚴格對齊仍然合理">何時混用 / 不嚴格對齊仍然合理</h2>
<p>「地區用語對齊」這條原則在公開技術文章成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼可以混用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用第三方資料 / quote</td>
          <td>引用 Apple Developer 的中文文件用「默認」、保留原文準確性比對齊重要</td>
      </tr>
      <tr>
          <td>業界共同術語（不分地區）</td>
          <td>「Bug」「commit」「pull request」這類本身就是英文、不在語料漂移範圍</td>
      </tr>
      <tr>
          <td>內部團隊溝通 / 工作日誌</td>
          <td>寫給知道讀者語料的特定群體、混用不損精度</td>
      </tr>
      <tr>
          <td>描述對方地區的具體系統</td>
          <td>「微信公眾號」「百度網盤」是大陸專有名詞、不該翻成台灣用詞</td>
      </tr>
      <tr>
          <td>引用作者自身用詞 / 個人風格保留</td>
          <td>訪談記錄 / 個人 blog 保留作者自己的語感是 narrative 選擇</td>
      </tr>
  </tbody>
</table>
<p>判讀：寫之前自問「讀者地區是否單一？我的用詞是否會讓讀者多花認知對映？」——是 → 嚴格對齊；否 → 混用可接受。</p>
<hr>
<h2 id="推到術語選擇的更上位原則">推到「術語選擇」的更上位原則</h2>
<p>地區用語對齊是「術語選擇」的子問題。更上位的原則：</p>
<blockquote>
<p>每個技術詞、寫之前都該確認「<strong>讀者語料是否包含這個詞</strong>」。</p></blockquote>
<p>這個自問適用於：</p>
<ul>
<li><strong>地區漂移</strong>：本卡主題（繁中 vs 簡中）</li>
<li><strong>學術 vs 業界用詞</strong>：「方法論」（學術）vs「做法」（業界）</li>
<li><strong>新舊術語</strong>：「響應式設計」（舊）vs「reactive 設計」（業界新）</li>
<li><strong>中文 vs 英文</strong>：技術術語第一次出現要不要保留原文錨點（見 <a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點</a>）</li>
<li><strong>完整名詞頭</strong>：壓縮中文術語要保留 head noun（見 <a href="../compressed-chinese-terms-need-head-noun/">#108 中文壓縮術語要保留完整名詞頭</a>）</li>
</ul>
<p>地區漂移是這個自問的第一層應用——讀者語料的最大差異訊號。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點</a></td>
          <td>兩卡都處理「讀者語料 ↔ 寫作用詞」對齊、本卡聚焦地區、#107 聚焦中英對應</td>
      </tr>
      <tr>
          <td><a href="../compressed-chinese-terms-need-head-noun/">#108 中文壓縮術語要保留完整名詞頭</a></td>
          <td>本卡是「術語完整性」的地區層、#108 是「術語完整性」的壓縮層</td>
      </tr>
      <tr>
          <td><a href="../translation-must-preserve-concept-role/">#109 術語翻譯要保留概念角色</a></td>
          <td>本卡是「概念→中文」的地區層、#109 是「概念→中文」的角色層</td>
      </tr>
      <tr>
          <td><a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度</a></td>
          <td>兩卡都處理「字句層級的精度」、本卡是地區漂移、#111 是修辭漂移</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>文章寫給台灣讀者、但混用兩邊用詞</td>
          <td>全篇 grep 替換、選定一個地區語料</td>
      </tr>
      <tr>
          <td>從中國技術文章複製範例 / 解釋</td>
          <td>翻譯時連用詞一起翻、不只翻句子</td>
      </tr>
      <tr>
          <td>用了某詞但不確定是不是地區漂移</td>
          <td>grep 「某詞 + 台灣 / 香港 + 中文」看用法分布</td>
      </tr>
      <tr>
          <td>內部討論用 A、寫對外文章用 B</td>
          <td>對外文章固定用讀者地區的版本、內部討論可寬鬆</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：技術寫作的精度標準是「讀者用自己的語料能正確 parse」。地區用語漂移是累積性的閱讀流暢度損失——單一詞對映成本不大、但整篇加起來顯著。寫前先確定讀者地區、寫完跑一次 grep 對齊。</p>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡的觸發是修 <a href="../../work-log/dart_stream_controller_single_vs_broadcast/">Dart StreamController：single-subscription vs broadcast 的事故實錄</a> 時、讀者指出「副屏」是中國用語、台灣應該用「副螢幕」。</p>
<p>修補時 grep 全文、發現「副屏」出現 9 處——不是孤立的一個詞、是整篇貫穿的用詞選擇。修補方式是 batch replace。</p>
<p>對應本卡：<strong>地區漂移多半不是孤立的、會在整篇文章貫穿——修補要用 grep 而不是逐句改。寫作前先確定讀者地區、寫完跑一次地區用語 sweep</strong>。</p>
]]></content:encoded></item><item><title>商業邏輯論述要 self-contained：不依賴 code 才能被理解</title><link>https://tarrragon.github.io/blog/report/prose-self-contained-without-code-reference/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/prose-self-contained-without-code-reference/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>1 個 case&lt;/strong>（&lt;a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故的 review&lt;/a> 中讀者指出「事件 payload 第二段」依賴 code 看過）抽出來的觀察。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Scope 限縮在概念說明 / 架構決策 / 設計檢討類文章&lt;/strong>：教學類、tutorial 類、code walkthrough 類文章的讀者本來就會逐行對映 code、本卡不適用——這幾類文章的論述跟 code 緊密交織是合理 narrative&lt;/li>
&lt;li>&lt;strong>「翻譯成業務角色」也有讀者熟悉度的邊界&lt;/strong>：用「鏡像訂閱者」「狀態變更 service」這類角色名詞替代「那個 controller」、對熟悉 POS 跟 pub-sub 的讀者通順、對不熟的讀者仍是空名詞。修法是「換 reference 類型」、不是「徹底解決 self-contained」&lt;/li>
&lt;li>&lt;strong>跟規則二（商業邏輯先於 CASE）的關係要精準&lt;/strong>：規則二講「層次順序」（先商業邏輯後 CASE）、本卡講「論述自包含」（不依賴 code reference）。兩者有重疊但不完全相同——本卡是規則二在「字句層 reference 處理」的子場景、不是規則二的全面延伸&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、先判斷文章類型——概念說明 / 架構決策 / 設計檢討 → 套用；教學 / tutorial → 評估「跟 code 交織」是否合理 narrative。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>概念說明 / 架構決策 / 設計檢討類文章的論述段（不放 code 的段落）要 &lt;strong>self-contained&lt;/strong>——用名詞 / 角色 / 條件描述業務邏輯、不依賴讀者去翻附近的 code block。讀者跳過所有 code block 仍能理解論述、是「商業邏輯先於 CASE」（compositional-writing 規則二）在「字句層 reference 處理」的子場景應用。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>依賴 code 的論述&lt;/th>
 &lt;th>Self-contained 論述&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>引用方式&lt;/td>
 &lt;td>「事件 payload 第二段帶了那個欄位」&lt;/td>
 &lt;td>「事件 payload 包含『當前完整列表』+『最後變動品項』兩段」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主詞&lt;/td>
 &lt;td>「那個 controller」「剛才的 service」&lt;/td>
 &lt;td>「副螢幕鏡像 controller」「狀態變更 service」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者前提&lt;/td>
 &lt;td>已經看過 code、記得結構&lt;/td>
 &lt;td>不需要看 code、只看論述&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗模式&lt;/td>
 &lt;td>讀者翻不到 reference 對應位置 → 卡住或誤讀&lt;/td>
 &lt;td>讀者直接 parse 論述、不被 code 結構綁住&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修補擴散&lt;/td>
 &lt;td>code 改了、論述自動 outdated&lt;/td>
 &lt;td>code 改了、論述仍然有效&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>self-contained 論述的價值在於&lt;strong>論述本身就是完整的、code 是 case 補充而非依賴&lt;/strong>。讀者用論述就能 reproduce 思考過程、code 提供具體驗證。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼依賴-code-的論述會出現">為什麼依賴 code 的論述會出現&lt;/h2>
&lt;h3 id="來源-1寫的人有-code-在腦中預設讀者也有">來源 1：寫的人有 code 在腦中、預設讀者也有&lt;/h3>
&lt;p>寫作者通常已經看過或寫過 code、所以在論述段用「那個 payload 第二段」這類 reference 對自己沒負擔。但讀者可能：&lt;/p>
&lt;ul>
&lt;li>跳過 code 直接讀論述（多數讀者習慣）&lt;/li>
&lt;li>看了 code 但沒記住具體結構&lt;/li>
&lt;li>不熟 Dart / 該專案、看 code 也 parse 不出 payload 結構&lt;/li>
&lt;/ul>
&lt;p>依賴 code 的論述把這些讀者擋在門外、強迫他們翻 code 對映、認知負擔顯著上升。&lt;/p>
&lt;h3 id="來源-2把對話風格搬進文章">來源 2：把對話風格搬進文章&lt;/h3>
&lt;p>「現在只要有人訂閱、把它記錄下來、UI 就能用」——這是對話風格、預設聽者跟說話者共享 context。寫成文章時要把 context 補進去：「需要新增一個訂閱者讀這段資訊、再把它對應到 UI 上的視覺標記」。&lt;/p>
&lt;h3 id="來源-3論述跟-code-段過於緊密交織">來源 3：論述跟 code 段過於緊密交織&lt;/h3>
&lt;p>文章在寫「先給 code、然後論述、然後再給 code」的交織結構時、論述容易自然 reference 上面 code 的具體行。讀者跳過 code 就斷掉。&lt;/p>
&lt;h3 id="來源-4誤把業務邏輯當成code-行為">來源 4：誤把「業務邏輯」當成「code 行為」&lt;/h3>
&lt;p>「業務邏輯」是「為什麼這件事存在 / 服務什麼需求」、「code 行為」是「具體怎麼跑」。依賴 code 的論述把兩者混在一起、讀者難以分離兩個層次。&lt;/p>
&lt;hr>
&lt;h2 id="識別訊號什麼時候你寫了依賴-code-的論述">識別訊號：什麼時候你寫了依賴 code 的論述&lt;/h2>
&lt;h3 id="訊號-1論述用那個這個剛才的上面的當主詞">訊號 1：論述用「那個」「這個」「剛才的」「上面的」當主詞&lt;/h3>
&lt;p>「那個 service」「這個 payload」「剛才的 controller」——這類代詞依賴讀者剛才看過 code。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>1 個 case</strong>（<a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故的 review</a> 中讀者指出「事件 payload 第二段」依賴 code 看過）抽出來的觀察。具體限制：</p>
<ul>
<li><strong>Scope 限縮在概念說明 / 架構決策 / 設計檢討類文章</strong>：教學類、tutorial 類、code walkthrough 類文章的讀者本來就會逐行對映 code、本卡不適用——這幾類文章的論述跟 code 緊密交織是合理 narrative</li>
<li><strong>「翻譯成業務角色」也有讀者熟悉度的邊界</strong>：用「鏡像訂閱者」「狀態變更 service」這類角色名詞替代「那個 controller」、對熟悉 POS 跟 pub-sub 的讀者通順、對不熟的讀者仍是空名詞。修法是「換 reference 類型」、不是「徹底解決 self-contained」</li>
<li><strong>跟規則二（商業邏輯先於 CASE）的關係要精準</strong>：規則二講「層次順序」（先商業邏輯後 CASE）、本卡講「論述自包含」（不依賴 code reference）。兩者有重疊但不完全相同——本卡是規則二在「字句層 reference 處理」的子場景、不是規則二的全面延伸</li>
</ul>
<p>讀者使用本卡時、先判斷文章類型——概念說明 / 架構決策 / 設計檢討 → 套用；教學 / tutorial → 評估「跟 code 交織」是否合理 narrative。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p>概念說明 / 架構決策 / 設計檢討類文章的論述段（不放 code 的段落）要 <strong>self-contained</strong>——用名詞 / 角色 / 條件描述業務邏輯、不依賴讀者去翻附近的 code block。讀者跳過所有 code block 仍能理解論述、是「商業邏輯先於 CASE」（compositional-writing 規則二）在「字句層 reference 處理」的子場景應用。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>依賴 code 的論述</th>
          <th>Self-contained 論述</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用方式</td>
          <td>「事件 payload 第二段帶了那個欄位」</td>
          <td>「事件 payload 包含『當前完整列表』+『最後變動品項』兩段」</td>
      </tr>
      <tr>
          <td>主詞</td>
          <td>「那個 controller」「剛才的 service」</td>
          <td>「副螢幕鏡像 controller」「狀態變更 service」</td>
      </tr>
      <tr>
          <td>讀者前提</td>
          <td>已經看過 code、記得結構</td>
          <td>不需要看 code、只看論述</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>讀者翻不到 reference 對應位置 → 卡住或誤讀</td>
          <td>讀者直接 parse 論述、不被 code 結構綁住</td>
      </tr>
      <tr>
          <td>修補擴散</td>
          <td>code 改了、論述自動 outdated</td>
          <td>code 改了、論述仍然有效</td>
      </tr>
  </tbody>
</table>
<p>self-contained 論述的價值在於<strong>論述本身就是完整的、code 是 case 補充而非依賴</strong>。讀者用論述就能 reproduce 思考過程、code 提供具體驗證。</p>
<hr>
<h2 id="為什麼依賴-code-的論述會出現">為什麼依賴 code 的論述會出現</h2>
<h3 id="來源-1寫的人有-code-在腦中預設讀者也有">來源 1：寫的人有 code 在腦中、預設讀者也有</h3>
<p>寫作者通常已經看過或寫過 code、所以在論述段用「那個 payload 第二段」這類 reference 對自己沒負擔。但讀者可能：</p>
<ul>
<li>跳過 code 直接讀論述（多數讀者習慣）</li>
<li>看了 code 但沒記住具體結構</li>
<li>不熟 Dart / 該專案、看 code 也 parse 不出 payload 結構</li>
</ul>
<p>依賴 code 的論述把這些讀者擋在門外、強迫他們翻 code 對映、認知負擔顯著上升。</p>
<h3 id="來源-2把對話風格搬進文章">來源 2：把對話風格搬進文章</h3>
<p>「現在只要有人訂閱、把它記錄下來、UI 就能用」——這是對話風格、預設聽者跟說話者共享 context。寫成文章時要把 context 補進去：「需要新增一個訂閱者讀這段資訊、再把它對應到 UI 上的視覺標記」。</p>
<h3 id="來源-3論述跟-code-段過於緊密交織">來源 3：論述跟 code 段過於緊密交織</h3>
<p>文章在寫「先給 code、然後論述、然後再給 code」的交織結構時、論述容易自然 reference 上面 code 的具體行。讀者跳過 code 就斷掉。</p>
<h3 id="來源-4誤把業務邏輯當成code-行為">來源 4：誤把「業務邏輯」當成「code 行為」</h3>
<p>「業務邏輯」是「為什麼這件事存在 / 服務什麼需求」、「code 行為」是「具體怎麼跑」。依賴 code 的論述把兩者混在一起、讀者難以分離兩個層次。</p>
<hr>
<h2 id="識別訊號什麼時候你寫了依賴-code-的論述">識別訊號：什麼時候你寫了依賴 code 的論述</h2>
<h3 id="訊號-1論述用那個這個剛才的上面的當主詞">訊號 1：論述用「那個」「這個」「剛才的」「上面的」當主詞</h3>
<p>「那個 service」「這個 payload」「剛才的 controller」——這類代詞依賴讀者剛才看過 code。</p>
<p>修法：把代詞換成具體名詞 + 角色描述（「狀態變更 service」「事件 payload」「副螢幕鏡像 controller」）。</p>
<h3 id="訊號-2用-code-結構描述第二段那個欄位">訊號 2：用 code 結構描述（「第二段」「那個欄位」）</h3>
<p>「payload 第二段」「那個 nullable 欄位」「上面 method 的 return value」——這類描述依賴讀者看過 code 的具體結構。</p>
<p>修法:把 code 結構描述翻譯成業務角色描述。「payload 第二段」→「最後變動品項欄位（給需要追蹤單筆變動的訂閱者用）」。</p>
<h3 id="訊號-3時序連接詞依賴-code-順序">訊號 3：時序連接詞依賴 code 順序</h3>
<p>「先&hellip;然後&hellip;接著&hellip;」如果這個時序對應上面 code 的執行順序、論述跟 code 綁太緊。</p>
<p>修法：把時序敘述為「在 X 條件下、Y 動作觸發 Z 結果」、不依賴 code 的具體順序。</p>
<h3 id="訊號-4論述只有就好就能就行">訊號 4：論述只有「就好」「就能」「就行」</h3>
<p>「現在只要有人訂閱、UI 就能用」「修改一行就好」——這類「就」字句<strong>在依賴 code 補足背景條件時</strong>有問題。但「修一行就好」如果背景條件已在前文說明、用「就」表達精簡是合理的——區分標準是「沒有 code 也讀得通嗎」、不是字面有「就」就違規。</p>
<p>修法：背景條件不在前文時、把省略的條件補回去；背景條件已在前文時、保留「就」沒問題。「在訂閱端讀取這段資訊、加一個視覺標記的綁定、即可在 UI 上呈現」。</p>
<h3 id="訊號-5跳過-code-block-後段落讀不通">訊號 5：跳過 code block 後段落讀不通</h3>
<p>最直接的測試方法：把所有 code block 拿掉、再讀一次論述、看是否仍能理解。</p>
<p>讀不通 → 論述依賴 code、要修補。</p>
<hr>
<h2 id="修法把-reference-翻成-self-contained-描述">修法：把 reference 翻成 self-contained 描述</h2>
<h3 id="修法-1用名詞替代代詞">修法 1：用名詞替代代詞</h3>
<p>修補前：</p>
<blockquote>
<p>「那個 service 對外發送事件、payload 第二段帶了這次變動是哪筆。」</p></blockquote>
<p>修補後：</p>
<blockquote>
<p>「狀態變更 service 對外發送的事件 payload 包含兩段：當前完整商品列表、最後變動的具體品項。第二段是『最後變動品項』。」</p></blockquote>
<h3 id="修法-2用角色替代位置">修法 2：用角色替代「位置」</h3>
<p>修補前：</p>
<blockquote>
<p>「上面 code 第三行那個 listener 拿到的是 nullable。」</p></blockquote>
<p>修補後：</p>
<blockquote>
<p>「副螢幕的訂閱者收到的事件 payload 中、第二段（最後變動品項）可能為 null（移除或清空操作的情境）。」</p></blockquote>
<h3 id="修法-3用條件替代時序">修法 3：用條件替代時序</h3>
<p>修補前：</p>
<blockquote>
<p>「先建立 controller、然後 listen、接著 add 事件、再 cancel。」</p></blockquote>
<p>修補後:</p>
<blockquote>
<p>「在 controller 建立後、訂閱者可呼叫 <code>.listen()</code> 註冊；註冊完成後、controller 才能 <code>.add()</code> 事件給訂閱者；訂閱者呼叫 <code>.cancel()</code> 解除註冊後、後續 <code>.listen()</code> 對 single-subscription 仍然違反契約。」</p></blockquote>
<h3 id="修法-4把就好展開成具體條件">修法 4：把「就好」展開成具體條件</h3>
<p>修補前：</p>
<blockquote>
<p>「現在只要有人訂閱、把它記錄下來、UI 就能用。」</p></blockquote>
<p>修補後：</p>
<blockquote>
<p>「新需求只要新增一個訂閱者讀這段資訊、再把它對應到 UI 上的視覺標記即可——介面不需要變動、payload 結構不需要調整、實作範圍只限於新增訂閱端。」</p></blockquote>
<hr>
<h2 id="何時論述可以依賴-code">何時論述可以依賴 code</h2>
<p>「論述要 self-contained」這條原則在概念說明 / 架構決策 / 設計檢討類文章成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼可以依賴 code</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Code walkthrough / line-by-line</td>
          <td>文章本身就是 code 解說、讀者預設會逐行對映</td>
      </tr>
      <tr>
          <td>簡短 inline 引用 specific 行為</td>
          <td>「<code>stream.listen()</code> 第一個參數是 callback」這類引用本身就是 code 字面</td>
      </tr>
      <tr>
          <td>Tutorial 教學步驟</td>
          <td>「跑這個指令、你會看到 X 輸出、接著做 Y」是 hands-on 教學風格</td>
      </tr>
      <tr>
          <td>Code review 評論</td>
          <td>評論本身就是針對某行 code、上下文是 inline 共享的</td>
      </tr>
  </tbody>
</table>
<p>判讀：寫之前自問「我的文章是『教讀者怎麼讀這份 code』還是『教讀者一個概念 / 框架』？」——前者可依賴 code、後者要 self-contained。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compositional-writing 規則二：商業邏輯先於 CASE</td>
          <td>本卡是規則二在「字句層 reference 處理」的子場景——規則二講層次順序、本卡講論述不依賴 code reference。兩者有重疊但不完全相同</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>用「那個 payload」是寫作便利、self-contained 論述需要刻意翻譯——同骨展現</td>
      </tr>
      <tr>
          <td><a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度</a></td>
          <td>「就好」「就能」這類字句既是口語也是依賴 code、本卡跟 #111 在這層重疊</td>
      </tr>
      <tr>
          <td><a href="../design-flaw-by-current-axes-not-hindsight/">#110 設計檢討用當下三軸論證、不依賴 hindsight</a></td>
          <td>三卡都是「論述要能讓讀者拿來用」的不同層次——當下視角 / 精度 / self-contained</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 段 / 第 Y 行 / 那個欄位」</td>
          <td>翻譯成業務角色描述</td>
      </tr>
      <tr>
          <td>段落用「就好 / 就能 / 就行」結尾</td>
          <td>把省略的條件補回去</td>
      </tr>
      <tr>
          <td>把 code block 拿掉後論述讀不通</td>
          <td>整段重寫成 self-contained</td>
      </tr>
      <tr>
          <td>寫的時候有 code 在記憶中、覺得「不用解釋」</td>
          <td>預設讀者跳過 code、強迫自己用文字描述</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：技術文章的論述要 self-contained——讀者跳過所有 code block 仍能理解論述、是「商業邏輯先於 CASE」的延伸實踐。寫完後跑一次「拿掉 code block 還讀得通嗎」自測、讀不通 → 翻譯成 self-contained 描述。</p>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡的觸發是修 <a href="../../work-log/dart_stream_controller_single_vs_broadcast/">Dart StreamController：single-subscription vs broadcast 的事故實錄</a> 時、讀者指出某段論述「重點混淆 + 過於口語 + 預設讀者看過原始碼」。</p>
<p>問題段（修補前）：</p>
<blockquote>
<p>「技術上這需求很乾淨：service 早就在事件 payload 第二段帶了『這次變動是哪筆』，現在只要有人訂閱、把它記錄下來，UI 就能用。於是收銀主畫面的 controller 加了第二個訂閱。」</p></blockquote>
<p>問題分析：</p>
<ul>
<li>「事件 payload 第二段」依賴讀者看過 code 結構</li>
<li>「就能用」「就好」省略了具體條件</li>
<li>「於是 X 加了 Y」對話風格、不是論述風格</li>
</ul>
<p>修補後：</p>
<blockquote>
<p>「這個需求剛好對應 service 已經備妥但尚未被消費的資訊——service 對外的事件 payload 從原始設計就分兩段：一段是『當前完整的商品列表』、另一段是『這次變動的具體品項』。第二段是當初為『需要追蹤單筆變動的訂閱者』預留的擴充欄位、過去幾個月一直沒被消費。新需求只要新增一個訂閱者讀這段資訊、再把它對應到 UI 上的視覺標記即可——介面不需要變動、payload 結構不需要調整、實作範圍只限於新增訂閱端。」</p></blockquote>
<p>修補後的論述用「角色描述（『需要追蹤單筆變動的訂閱者』）」替代了「位置描述（『第二段』）」、補回了省略的條件（「介面不需要變動、payload 結構不需要調整、實作範圍只限於新增訂閱端」）。讀者即使跳過所有 code block 也能理解這段論述。</p>
<p>對應本卡：<strong>論述依賴 code 是「寫的人有 code 在腦中」的便利選擇——multi-pass review 要在輪 5「反例 / 邊界」加掃 self-contained frame、用「拿掉 code 還讀得通嗎」自測</strong>。</p>
]]></content:encoded></item><item><title>Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整</title><link>https://tarrragon.github.io/blog/report/multi-pass-review-frame-granularity-blindspot/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/multi-pass-review-frame-granularity-blindspot/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>1 個 case&lt;/strong>（&lt;a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故&lt;/a> 的 review 過程跑 4 輪後仍漏 catch 4 類字句層問題）抽出來的假說。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>樣本量極小&lt;/strong>：「multi-pass review framework 顆粒度盲點」這個結論基於 1 次 review、不是多次跨主題 review 觀察到的 systematic pattern。可能是這個 reviewer（我）有特定盲點、不是 framework 本身的問題&lt;/li>
&lt;li>&lt;strong>三機制有效性未驗證&lt;/strong>：keyword bank / reader simulation / self-criticism 三機制是 proposed mechanisms、未實際跑下一篇文章驗證能 catch 之前漏掉的問題類型&lt;/li>
&lt;li>&lt;strong>「reader simulation 由同 reviewer 執行」的根本質疑沒解決&lt;/strong>：拿掉 code block 重讀、聽起來合理、但同一個人是否真能模擬「沒看過 code 的讀者視角」是疑問——記憶不會因為 code block 隱藏而消失。本卡提的修法是 partial fix、不是 root cause solution&lt;/li>
&lt;li>&lt;strong>「同一 reviewer 跑多輪 catch 高度相同」是直覺論述&lt;/strong>：沒有實證、是直覺推論&lt;/li>
&lt;li>&lt;strong>跟其他卡互相 cross-link 形成迴圈論證&lt;/strong>：本卡引 #110 / #111 / #113、但這幾張卡都源自同次事件、互相驗證有 selection bias 風險&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、把它當「&lt;strong>從一次 review 失誤抽出的盲點假說&lt;/strong>」、不當「驗證過的 review framework 升級方案」。三機制是 starting point、有效性需要後續案例累積驗證。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>Multi-pass review 用「規則 frame」掃描、有效抓「結構性違反」（規則順序、論述結構、邊界段缺失）、但&lt;strong>這次 case 顯示對「字句層的具體訊號」覆蓋不足&lt;/strong>——同個規則底下有大量具體訊號、reviewer 用記憶 sweep 容易漏掉一部分（這次 1 個 case 觀察、是否 systematic 有待累積）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>缺口類型&lt;/th>
 &lt;th>Multi-pass 用「規則 frame」能抓&lt;/th>
 &lt;th>Multi-pass 用「規則 frame」抓不到&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>術語原文錨點（contract / 領域先驗）&lt;/td>
 &lt;td>地區漂移（屏 / 螢幕、默認 / 預設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>論述自包含性&lt;/td>
 &lt;td>H2 後加商業邏輯導引&lt;/td>
 &lt;td>段落內依賴 code（「payload 第二段」）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>句型結構&lt;/td>
 &lt;td>反例段落補正向錨點（明顯）&lt;/td>
 &lt;td>「不是 A 而是 B」結構（隱性違反）+ 廢話前綴 wrapper&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵差異是「規則理解」vs「具體訊號比對」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>規則理解&lt;/strong>：reviewer 知道「正向陳述優先」這條規則&lt;/li>
&lt;li>&lt;strong>具體訊號比對&lt;/strong>：reviewer 要逐句檢查所有可能違反該規則的具體句型&lt;/li>
&lt;/ul>
&lt;p>抽象規則 → 具體訊號的轉譯沒做完整、就會 systematic miss 一整類字句層問題。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼規則-frame抓不到字句層問題">為什麼「規則 frame」抓不到字句層問題&lt;/h2>
&lt;h3 id="問題-1抽象規則沒展開成具體訊號清單">問題 1：抽象規則沒展開成具體訊號清單&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>廢話前綴 / wrapper&lt;/td>
 &lt;td>「下次看到 X 時、做 Y」&lt;/td>
 &lt;td>結尾段、heuristic 段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>觀察先 / 定義後&lt;/td>
 &lt;td>「實務上常看到：[code]」&lt;/td>
 &lt;td>起點段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>否定先 / 肯定後&lt;/td>
 &lt;td>「不要先想 A、先想 B」&lt;/td>
 &lt;td>除錯思維、check list&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>條件先 / 結論後&lt;/td>
 &lt;td>「在 X、Y、Z 條件下、結論是 W」&lt;/td>
 &lt;td>推導段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修飾先 / 主詞後&lt;/td>
 &lt;td>「考慮所有可能後、做 X」&lt;/td>
 &lt;td>提案段&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>reviewer 用「規則五」這個 frame 掃描、靠記憶找「這段有沒有違反規則五」——多半只 catch「觀察先 / 定義後」這個明顯 case、漏 catch 廢話前綴跟否定先行。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>1 個 case</strong>（<a href="../../work-log/dart_stream_controller_single_vs_broadcast/">dart Stream 事故</a> 的 review 過程跑 4 輪後仍漏 catch 4 類字句層問題）抽出來的假說。具體限制：</p>
<ul>
<li><strong>樣本量極小</strong>：「multi-pass review framework 顆粒度盲點」這個結論基於 1 次 review、不是多次跨主題 review 觀察到的 systematic pattern。可能是這個 reviewer（我）有特定盲點、不是 framework 本身的問題</li>
<li><strong>三機制有效性未驗證</strong>：keyword bank / reader simulation / self-criticism 三機制是 proposed mechanisms、未實際跑下一篇文章驗證能 catch 之前漏掉的問題類型</li>
<li><strong>「reader simulation 由同 reviewer 執行」的根本質疑沒解決</strong>：拿掉 code block 重讀、聽起來合理、但同一個人是否真能模擬「沒看過 code 的讀者視角」是疑問——記憶不會因為 code block 隱藏而消失。本卡提的修法是 partial fix、不是 root cause solution</li>
<li><strong>「同一 reviewer 跑多輪 catch 高度相同」是直覺論述</strong>：沒有實證、是直覺推論</li>
<li><strong>跟其他卡互相 cross-link 形成迴圈論證</strong>：本卡引 #110 / #111 / #113、但這幾張卡都源自同次事件、互相驗證有 selection bias 風險</li>
</ul>
<p>讀者使用本卡時、把它當「<strong>從一次 review 失誤抽出的盲點假說</strong>」、不當「驗證過的 review framework 升級方案」。三機制是 starting point、有效性需要後續案例累積驗證。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p>Multi-pass review 用「規則 frame」掃描、有效抓「結構性違反」（規則順序、論述結構、邊界段缺失）、但<strong>這次 case 顯示對「字句層的具體訊號」覆蓋不足</strong>——同個規則底下有大量具體訊號、reviewer 用記憶 sweep 容易漏掉一部分（這次 1 個 case 觀察、是否 systematic 有待累積）。</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>Multi-pass 用「規則 frame」能抓</th>
          <th>Multi-pass 用「規則 frame」抓不到</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>結構性違反</td>
          <td>段落順序、論述結構、邊界段缺失</td>
          <td>—</td>
      </tr>
      <tr>
          <td>規則對齊</td>
          <td>「應該 / 必須」絕對主義（明顯）</td>
          <td>「碰巧 / 撞牆 / 一輩子」口語修辭（同樣違反但不明顯）</td>
      </tr>
      <tr>
          <td>用詞精度</td>
          <td>術語原文錨點（contract / 領域先驗）</td>
          <td>地區漂移（屏 / 螢幕、默認 / 預設）</td>
      </tr>
      <tr>
          <td>論述自包含性</td>
          <td>H2 後加商業邏輯導引</td>
          <td>段落內依賴 code（「payload 第二段」）</td>
      </tr>
      <tr>
          <td>句型結構</td>
          <td>反例段落補正向錨點（明顯）</td>
          <td>「不是 A 而是 B」結構（隱性違反）+ 廢話前綴 wrapper</td>
      </tr>
  </tbody>
</table>
<p>關鍵差異是「規則理解」vs「具體訊號比對」：</p>
<ul>
<li><strong>規則理解</strong>：reviewer 知道「正向陳述優先」這條規則</li>
<li><strong>具體訊號比對</strong>：reviewer 要逐句檢查所有可能違反該規則的具體句型</li>
</ul>
<p>抽象規則 → 具體訊號的轉譯沒做完整、就會 systematic miss 一整類字句層問題。</p>
<hr>
<h2 id="為什麼規則-frame抓不到字句層問題">為什麼「規則 frame」抓不到字句層問題</h2>
<h3 id="問題-1抽象規則沒展開成具體訊號清單">問題 1：抽象規則沒展開成具體訊號清單</h3>
<p>每條規則有大量可能的違反句型——例如「規則五：最重要的話優先說」可能違反句型：</p>
<table>
  <thead>
      <tr>
          <th>違反句型</th>
          <th>具體案例</th>
          <th>在哪裡常見</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>廢話前綴 / wrapper</td>
          <td>「下次看到 X 時、做 Y」</td>
          <td>結尾段、heuristic 段</td>
      </tr>
      <tr>
          <td>觀察先 / 定義後</td>
          <td>「實務上常看到：[code]」</td>
          <td>起點段</td>
      </tr>
      <tr>
          <td>否定先 / 肯定後</td>
          <td>「不要先想 A、先想 B」</td>
          <td>除錯思維、check list</td>
      </tr>
      <tr>
          <td>條件先 / 結論後</td>
          <td>「在 X、Y、Z 條件下、結論是 W」</td>
          <td>推導段</td>
      </tr>
      <tr>
          <td>修飾先 / 主詞後</td>
          <td>「考慮所有可能後、做 X」</td>
          <td>提案段</td>
      </tr>
  </tbody>
</table>
<p>reviewer 用「規則五」這個 frame 掃描、靠記憶找「這段有沒有違反規則五」——多半只 catch「觀察先 / 定義後」這個明顯 case、漏 catch 廢話前綴跟否定先行。</p>
<h3 id="問題-2缺乏-grep-keyword-bank">問題 2：缺乏 grep keyword bank</h3>
<p>字句層問題有大量可 grep 的具體詞——但 reviewer 沒有 keyword bank、靠肉眼掃。例如：</p>
<table>
  <thead>
      <tr>
          <th>規則類別</th>
          <th>可 grep 的具體詞</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>口語修辭</td>
          <td>一輩子 / 永遠 / 碰巧 / 剛好 / 撞牆 / 炸 / 鎖死 / 啊原來</td>
      </tr>
      <tr>
          <td>廢話前綴</td>
          <td>下次看到 / 下次寫 / 下次面對 / 下次遇到 / 之後再</td>
      </tr>
      <tr>
          <td>否定先行</td>
          <td>不要先 / 不是 A 而是 B / 不該 / 不能</td>
      </tr>
      <tr>
          <td>地區漂移</td>
          <td>屏 / 默認 / 質量 / 視頻 / 文件（當 file 用）</td>
      </tr>
      <tr>
          <td>依賴 code</td>
          <td>那個 / 這個 / 剛才的 / 上面的 / 第 X 段 / 就好 / 就能</td>
      </tr>
  </tbody>
</table>
<p>每輪 review 用 grep 比對固定 keyword list、不靠 reviewer 記憶——能消除「靠記憶找違規」的 systematic miss。</p>
<h3 id="問題-3reviewer-自我審查的視角盲點">問題 3：reviewer 自我審查的視角盲點</h3>
<p>reviewer 讀自己寫的東西、會自動 fill in 上下文、感受不到讀者的真實閱讀體驗。例如：</p>
<ul>
<li>「事件 payload 第二段帶了 X」——reviewer 寫的時候腦中有 code、知道「第二段」是什麼、感覺通順</li>
<li>讀者讀的時候沒有 code 在腦中、「第二段」是空的 reference、卡住</li>
</ul>
<p>這個視角差異是 multi-pass review 的結構性盲點——同一個 reviewer 跑多輪、視角始終是寫作者視角、不是讀者視角。</p>
<h3 id="問題-4multi-pass-缺-self-criticism-輪">問題 4：Multi-pass 缺 self-criticism 輪</h3>
<p>每輪 review 都是 forward checking（這篇對齊規則嗎？）、沒做 backward critique（規則本身在這個情境是否夠細？有沒有 miss 的 frame？）。</p>
<p>如果規則框架本身不夠細、跑再多輪都掃不到 frame 之外的問題。</p>
<hr>
<h2 id="多面向四類-missed-問題的分類">多面向：四類 missed 問題的分類</h2>
<p>這次跑完 4 輪 multi-pass review、漏 catch 的 4 類問題：</p>
<h3 id="miss-類型-1口語修辭規則七--規則五的字句層子場景">Miss 類型 1：口語修辭（規則七 / 規則五的字句層子場景）</h3>
<p>漏 catch 的具體訊號:「一輩子只能 listen 一次」「碰巧能用」「立刻撞牆」「啊原來」「炸了」</p>
<p><strong>為什麼漏</strong>：「規則七：機會成本語氣」掃了「應該/必須/不行」、沒掃「一輩子/碰巧/撞牆」這類修辭詞。修辭詞跟絕對主義詞屬於不同 keyword set、reviewer 沒同時掃。</p>
<p><strong>修法</strong>：建立「口語修辭 keyword bank」、輪 4「術語精度」加掃。</p>
<h3 id="miss-類型-2地區漂移規則四術語的子場景">Miss 類型 2：地區漂移（規則四「術語」的子場景）</h3>
<p>漏 catch 的具體訊號：「副屏」（中國用語、繁中應該用「副螢幕」）</p>
<p><strong>為什麼漏</strong>：輪 4「術語檢查」聚焦在「中文 / 原文錨點」、沒掃「繁中 / 簡中漂移」。reviewer 預設「讀者地區」是台灣、但沒 explicit 用 keyword bank 比對。</p>
<p><strong>修法</strong>：建立「地區用語對齊 keyword bank」（屏 / 默認 / 質量 / 視頻 / 文件 / 函數 / 介面 / 內存）、輪 4 加掃。</p>
<h3 id="miss-類型-3依賴-code-論述規則二商業邏輯先於-case-的延伸">Miss 類型 3：依賴 code 論述（規則二商業邏輯先於 CASE 的延伸）</h3>
<p>漏 catch 的具體訊號：「事件 payload 第二段帶了 X」「就好 / 就能」</p>
<p><strong>為什麼漏</strong>：規則二被理解成「H2 後加商業邏輯導引」、沒延伸到「論述本身不依賴 code」。reviewer 寫的時候腦中有 code、感受不到「依賴 code」的閱讀體驗。</p>
<p><strong>修法</strong>：加「reader simulation」frame——拿掉所有 code block、再讀一次論述、看是否仍能理解。</p>
<h3 id="miss-類型-4廢話前綴--否定先行規則五--規則六的字句層子場景">Miss 類型 4：廢話前綴 + 否定先行（規則五 + 規則六的字句層子場景）</h3>
<p>漏 catch 的具體訊號：「下次看到 X 時、不要先想 Y」這類 hortative 結尾段</p>
<p><strong>為什麼漏</strong>：規則五「最重要的話優先說」被理解成「核心原則先 / 例子後」、沒延伸到「廢話前綴 wrapper 句子」。規則六「正向陳述」被理解成「反例段落補正向錨點」、沒延伸到「『不是 A 而是 B』結構」。</p>
<p><strong>修法</strong>：建立「廢話前綴 + 否定先行 keyword bank」、輪 5 加掃。</p>
<hr>
<h2 id="修補-multi-pass-review-框架的三個機制">修補 multi-pass review 框架的三個機制</h2>
<h3 id="機制-1keyword-bank具體訊號清單">機制 1：Keyword bank（具體訊號清單）</h3>
<p>每條規則展開成可 grep 的 keyword list、每輪 review 用 grep 比對、不靠 reviewer 記憶。</p>
<p>範例 keyword bank（節選）：</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">  下次看到 / 下次寫 / 下次面對 / 下次遇到 / 不要先 / 不是 X 而是 Y
</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><span class="line"><span class="ln"> 8</span><span class="cl">  屏 / 默認 / 質量 / 視頻 / 文件（當 file）/ 函數 / 接口 / 內存 / 視頻
</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">依賴 code 訊號：
</span></span><span class="line"><span class="ln">11</span><span class="cl">  那個 / 這個 / 剛才的 / 上面的 / 第 X 段 / 就好 / 就能 / 就行</span></span></code></pre></div><p>每篇文章 review 時跑這些 grep、把 hit 列出來、決定保留或修補。</p>
<h3 id="機制-2reader-simulation-輪">機制 2：Reader simulation 輪</h3>
<p>加一輪「假設讀者沒有上下文、能不能讀懂這段論述」、嘗試換視角。具體做法：</p>
<ul>
<li><strong>拿掉所有 code block 後重讀</strong>：論述是否 self-contained？</li>
<li><strong>跳到段落中間直接讀</strong>：不依賴前文、能不能 parse？</li>
<li><strong>隨機抽段給陌生讀者讀</strong>：cold-read 能不能拿到關鍵資訊？</li>
</ul>
<p><strong>已知限制</strong>：同一 reviewer 即使拿掉 code block、記憶仍在、無法完全模擬「沒看過 code 的讀者視角」。這個機制是 partial fix——能 catch 部分上下文依賴、但不是 root cause solution。最終解法仍需引入外部讀者反饋（cold-read by 真實讀者）。</p>
<h3 id="機制-3self-criticism-輪">機制 3：Self-criticism 輪</h3>
<p>加一輪「我這份規則本身在這個情境是否夠細、有沒有 miss 的 frame？」、強迫 reviewer 反向審視框架本身。具體 prompt：</p>
<ul>
<li>「我跑的 N 輪、catch 的問題類型有哪些？」</li>
<li>「同個規則底下、還有哪些可能違反句型沒被掃到？」</li>
<li>「如果讀者報告 X 類問題、是哪輪該 catch 但沒 catch？」</li>
<li>「framework 本身是否有 known blind spot？」</li>
</ul>
<p>self-criticism 輪不是「再跑一次規則 frame」、是「<strong>檢視 frame 本身的覆蓋度</strong>」。</p>
<hr>
<h2 id="為什麼這些機制不能被再仔細一輪取代">為什麼這些機制不能被「再仔細一輪」取代</h2>
<h3 id="再仔細一輪的同-frame-盲點">「再仔細一輪」的同 frame 盲點</h3>
<p>reviewer 跑同一個 frame 兩次、catch 的東西<strong>多半</strong>高度相同——因為視角、知識、注意力分配相同。（這是直覺論述、未做受控實驗驗證；但跟「換 frame」的設計動機一致——multi-pass 的核心就是「同 frame 重看 catch 不到新問題」）Multi-pass review 的核心是「每輪換 frame」、不是「同 frame 多跑幾次」。</p>
<p>但<strong>換 frame ≠ 換規則</strong>——reviewer 可能換規則但用同樣的視角、同樣的記憶 sweep、catch 的東西相同。要真正換 frame、需要：</p>
<ul>
<li><strong>換工具</strong>：keyword bank 取代肉眼掃（機制 1）</li>
<li><strong>換視角</strong>：模擬讀者取代 reviewer 視角（機制 2）</li>
<li><strong>換層次</strong>：審視 framework 取代套用 framework（機制 3）</li>
</ul>
<p>三個機制各自處理「同一 reviewer 跑多輪仍 miss」的不同來源。</p>
<h3 id="hindsight-視角的反向印證">Hindsight 視角的反向印證</h3>
<p><a href="../design-flaw-by-current-axes-not-hindsight/">#110 設計檢討用當下三軸論證、不依賴 hindsight</a> 的核心議題是「事後諸葛論述」會混淆「設計缺陷 vs 需求演化」。同樣的 hindsight 風險也存在於 review 流程：</p>
<ul>
<li><strong>Hindsight 視角</strong>：「讀者反饋了 → 補進規則」——把規則當成「事故後補的 patch」</li>
<li><strong>當下三軸視角</strong>：「framework 本身是否夠細到 catch 這類問題？」——把 framework 當成預設工具、用 self-criticism 反向審視</li>
</ul>
<p>兩種視角的差別跟 #110 的差別同骨：前者依賴結局（讀者反饋）、後者用當下框架審視（self-criticism）。</p>
<hr>
<h2 id="識別訊號什麼時候你的-review-framework-不夠細">識別訊號：什麼時候你的 review framework 不夠細</h2>
<h3 id="訊號-1讀者反饋的問題類型在-framework-裡找不到對應-frame">訊號 1：讀者反饋的問題類型在 framework 裡找不到對應 frame</h3>
<p>讀者指出「廢話前綴」問題、reviewer 翻 framework 找對應 frame——找到「規則五最重要的話優先說」、但這條規則沒展開到「廢話前綴」這個具體子場景。</p>
<p>修法：把問題類型加進 framework 的 keyword bank、下次同類問題能被 grep catch。</p>
<h3 id="訊號-2跑了-n-輪相同類型的問題仍重複出現">訊號 2：跑了 N 輪、相同類型的問題仍重複出現</h3>
<p>字句層問題（口語修辭、地區漂移）跑了 4 輪 review 仍漏——表示 framework 沒 catch 這個層次。</p>
<p>修法：加 keyword bank（機制 1）、不靠 reviewer 記憶。</p>
<h3 id="訊號-3reviewer-自我審查感覺通順讀者反映卡住">訊號 3：reviewer 自我審查感覺通順、讀者反映卡住</h3>
<p>「事件 payload 第二段」對 reviewer 通順、對讀者卡住——視角差異。</p>
<p>修法：加 reader simulation 輪（機制 2）。</p>
<h3 id="訊號-4相同-framework-跑不同主題catch-的問題類型差異不大">訊號 4：相同 framework 跑不同主題、catch 的問題類型差異不大</h3>
<p>framework 不會自我批判——跑 100 篇文章、catch 的都是 framework 內的 frame、framework 外的問題永遠看不見。</p>
<p>修法：加 self-criticism 輪（機制 3）、定期審視 framework 本身的覆蓋度。</p>
<hr>
<h2 id="何時不需要這些補強機制">何時不需要這些補強機制</h2>
<p>「multi-pass review 需要 keyword bank + reader simulation + self-criticism」這條原則在 production 教學文章 / 設計檢討文章成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不需要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>短篇 note / 即時更新</td>
          <td>預期讀者群小、不擴散、字句層問題影響有限</td>
      </tr>
      <tr>
          <td>個人筆記</td>
          <td>reviewer = reader、視角差異不存在</td>
      </tr>
      <tr>
          <td>Review framework 已成熟、團隊內化</td>
          <td>keyword bank 已經內化成 reviewer 的反射、不需要 explicit 工具</td>
      </tr>
      <tr>
          <td>Framework 規模太小</td>
          <td>framework 只有 3-5 條規則時、self-criticism 容易出 false positive</td>
      </tr>
  </tbody>
</table>
<p>判讀：寫之前自問「這篇文章的讀者群有多大？字句層問題擴散的代價有多高？」——大 / 高 → 嚴格用三機制；小 / 低 → 可放寬。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Multi-pass review</a></td>
          <td>本卡是 #83 的延伸——#83 講「每輪換 frame」、本卡講「frame 本身要夠細、且需要工具 / 視角 / 層次三軸補強」</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>用「規則 frame 掃描」是 reviewer 的寫作便利、用「keyword bank + reader simulation」是費力但精準</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review 的 scope 要蓋同類風險區</a></td>
          <td>#95 處理「scope 軸」（review 多廣）、本卡處理「frame 顆粒度軸」（規則展開多細）、兩軸正交</td>
      </tr>
      <tr>
          <td><a href="../design-flaw-by-current-axes-not-hindsight/">#110 設計檢討用當下三軸論證、不依賴 hindsight</a></td>
          <td>hindsight 視角會把 review framework 當「補丁」、self-criticism 用當下框架審視、跟 #110 同骨</td>
      </tr>
      <tr>
          <td><a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度</a></td>
          <td>#111 是字句層的「具體訊號」之一、本卡是「為什麼字句層訊號被 framework 漏 catch」的 meta 層</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>讀者反饋了 framework 裡找不到對應 frame</td>
          <td>加進 keyword bank、補進 framework 的 frame 列表</td>
      </tr>
      <tr>
          <td>跑 N 輪後同類問題仍出現</td>
          <td>framework 不夠細、加機制 1（keyword bank）</td>
      </tr>
      <tr>
          <td>reviewer 通順 / 讀者卡住</td>
          <td>加機制 2（reader simulation 輪）</td>
      </tr>
      <tr>
          <td>framework 從來沒被質疑過</td>
          <td>加機制 3（self-criticism 輪）、定期審視 framework 本身</td>
      </tr>
      <tr>
          <td>多輪 review 跑完還是同 reviewer</td>
          <td>引入外部讀者反饋、或刻意換視角（不同 IDE / 不同字體 / 跳段順序讀）</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：multi-pass review 用「規則 frame」掃描有效抓結構性違反、抓不到字句層具體訊號。要 catch 字句層、需要把規則展開成 keyword bank、加 reader simulation 視角、加 self-criticism 反向審視 framework 本身——三個機制各自處理同 reviewer 跑多輪仍 miss 的不同來源。</p>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡的觸發是修 <a href="../../work-log/dart_stream_controller_single_vs_broadcast/">Dart StreamController：single-subscription vs broadcast 的事故實錄</a> 時、跑了 4 輪 multi-pass review 後仍漏 catch 4 類字句層問題、由讀者點出。</p>
<p>讀者反饋的問題類型：</p>
<ol>
<li>口語化修辭（「一輩子只能 listen 一次」「立刻撞牆」「啊原來」「碰巧能用」）</li>
<li>地區用語漂移（「副屏」是中國用語、台灣用「副螢幕」）</li>
<li>依賴 code 論述（「事件 payload 第二段帶了」預設讀者看過 code）</li>
<li>廢話前綴 + 否定先行（「下次看到 X 時、不要先想 Y、先問 Z」）</li>
</ol>
<p>這 4 類問題對應的 frame 在 framework 裡都有（規則七機會成本、輪 4 術語、規則二商業邏輯、規則五最重要的話優先說）——但都沒展開到具體訊號層、所以 reviewer 跑了 4 輪都漏 catch。</p>
<p>對應本卡：<strong>framework 的覆蓋度盲點不能靠「再仔細一輪」修補——同一 reviewer 跑同 framework 多輪、catch 的東西高度相同。要真正擴大覆蓋度、需要 keyword bank（換工具）+ reader simulation（換視角）+ self-criticism（換層次）三個機制</strong>。</p>
<p>對 multi-pass review framework 的修補方向：把這 4 類問題加進對應 frame 的 keyword bank、加 reader simulation 輪當輪 8、加 self-criticism 輪當輪 9（或在每輪結尾加 self-criticism 子段）。</p>
]]></content:encoded></item><item><title>案例引用深度跟著 case 類型走</title><link>https://tarrragon.github.io/blog/report/case-type-graded-citation-depth/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/case-type-graded-citation-depth/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>案例引用深度是寫作層的紀律工具：把 case 分成 skeleton / medium / rich 三類、各類對應不同承接句型、讓引用斷言能反向 trace 回 case 原文。誤判類型會引發 over-extrapolation（編造 case 沒寫的細節）或 under-citation（漏掉 case 揭露的 mechanism）、reviewer 抓出後修正成本高。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Case 類型&lt;/th>
 &lt;th>行數&lt;/th>
 &lt;th>內容密度&lt;/th>
 &lt;th>承接句型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Skeleton case&lt;/td>
 &lt;td>10-30 行&lt;/td>
 &lt;td>只給方向、無具體數字 / taxonomy&lt;/td>
 &lt;td>「揭露 X 方向、以下基於通用工程知識補充」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Medium case&lt;/td>
 &lt;td>30-50 行&lt;/td>
 &lt;td>結構化 mechanism + 訊號名稱、無具體數字&lt;/td>
 &lt;td>「揭露 N 個機制 — A、B、C、D」用 mechanism 名稱精準引用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rich case&lt;/td>
 &lt;td>50-200 行&lt;/td>
 &lt;td>含具體數字（RPS / 延遲 / TPS）、設計細節&lt;/td>
 &lt;td>「揭露 X 觀察 + 作者判讀 Y（明示分層）」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跨類型混合引用要分層處理 — 同段內若引兩類 case、先寫 rich case fact 作為支撐、再用 skeleton case 補方向、不混合成單一斷言。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼這層紀律重要">為什麼這層紀律重要&lt;/h2>
&lt;p>LLM 從 case 反推內容時、訓練資料的「通用知識」會自動補進章節。當 case 沒寫的 mechanism / 數字 / taxonomy 被寫成「對應 [case]：揭露 X」斷言、讀者回查 case 時會發現章節說的「揭露」實際是 LLM 編造。&lt;/p>
&lt;p>backend/01-07 七模組驗證（&lt;a href="../../posts/case-first-agent-team-review-workflow/">case-first workflow&lt;/a> 的 case fidelity reviewer 抓的數據）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模組&lt;/th>
 &lt;th>主要 case 類型&lt;/th>
 &lt;th>Case fidelity&lt;/th>
 &lt;th>主要失分類型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>backend/02 cache&lt;/td>
 &lt;td>Skeleton&lt;/td>
 &lt;td>78%&lt;/td>
 &lt;td>三層 cache latency 編造、ops/sec 編造&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>backend/03 msgq&lt;/td>
 &lt;td>Skeleton&lt;/td>
 &lt;td>70%（最低）&lt;/td>
 &lt;td>三方向擴寫成「4 個誤配場景」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>backend/04 obs&lt;/td>
 &lt;td>Skeleton&lt;/td>
 &lt;td>92.9%（最高）&lt;/td>
 &lt;td>嚴守「揭露方向、通用補充」紀律&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>backend/05 deploy&lt;/td>
 &lt;td>Skeleton + Rich&lt;/td>
 &lt;td>80%&lt;/td>
 &lt;td>Rich case 判讀層被當 fact 引用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>backend/06 reliability&lt;/td>
 &lt;td>Medium 全套&lt;/td>
 &lt;td>88%&lt;/td>
 &lt;td>Medium case 實作層擴寫過頭&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>backend/07 batch 1&lt;/td>
 &lt;td>Medium + Skeleton&lt;/td>
 &lt;td>81%&lt;/td>
 &lt;td>跨 case 合成 frame 升級成 case 揭露&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最高的 92.9% 跟最低的 70% 差 23 個百分點 — 關鍵變數是引用紀律、案例品質本身只佔次要因素。Skeleton case 嚴守「揭露方向」、不擴寫成 rich case 樣式、就能達到 90%+。&lt;/p>
&lt;hr>
&lt;h2 id="三類-case-的判讀條件">三類 case 的判讀條件&lt;/h2>
&lt;h3 id="skeleton-case揭露-x-方向通用補充">Skeleton case：「揭露 X 方向、通用補充」&lt;/h3>
&lt;p>典型：模組內部 N.Cx 案例庫中只有 frame、無具體數字的短篇 case。內容深度 10-30 行、給「議題」「視角」、不給「實作細節」。&lt;/p>
&lt;p>承接紀律：&lt;/p>
&lt;ul>
&lt;li>引用為「揭露 X 方向」、不引用為「揭露 N 個具體場景數量」&lt;/li>
&lt;li>後面補「以下展開基於通用工程知識補充」明示分層&lt;/li>
&lt;li>不為了「整齊的 4 個攻擊面」「3 個攻擊向量」這種數字感、把 case 沒寫的 taxonomy 寫成 case 揭露&lt;/li>
&lt;/ul>
&lt;p>例：Meta Cache Consistency case 只給「promotion、shard move、故障恢復」三個方向 → 引用為「揭露三個方向」、不引用為「揭露具體 inconsistency window 數字」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>案例引用深度是寫作層的紀律工具：把 case 分成 skeleton / medium / rich 三類、各類對應不同承接句型、讓引用斷言能反向 trace 回 case 原文。誤判類型會引發 over-extrapolation（編造 case 沒寫的細節）或 under-citation（漏掉 case 揭露的 mechanism）、reviewer 抓出後修正成本高。</p>
<table>
  <thead>
      <tr>
          <th>Case 類型</th>
          <th>行數</th>
          <th>內容密度</th>
          <th>承接句型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Skeleton case</td>
          <td>10-30 行</td>
          <td>只給方向、無具體數字 / taxonomy</td>
          <td>「揭露 X 方向、以下基於通用工程知識補充」</td>
      </tr>
      <tr>
          <td>Medium case</td>
          <td>30-50 行</td>
          <td>結構化 mechanism + 訊號名稱、無具體數字</td>
          <td>「揭露 N 個機制 — A、B、C、D」用 mechanism 名稱精準引用</td>
      </tr>
      <tr>
          <td>Rich case</td>
          <td>50-200 行</td>
          <td>含具體數字（RPS / 延遲 / TPS）、設計細節</td>
          <td>「揭露 X 觀察 + 作者判讀 Y（明示分層）」</td>
      </tr>
  </tbody>
</table>
<p>跨類型混合引用要分層處理 — 同段內若引兩類 case、先寫 rich case fact 作為支撐、再用 skeleton case 補方向、不混合成單一斷言。</p>
<hr>
<h2 id="為什麼這層紀律重要">為什麼這層紀律重要</h2>
<p>LLM 從 case 反推內容時、訓練資料的「通用知識」會自動補進章節。當 case 沒寫的 mechanism / 數字 / taxonomy 被寫成「對應 [case]：揭露 X」斷言、讀者回查 case 時會發現章節說的「揭露」實際是 LLM 編造。</p>
<p>backend/01-07 七模組驗證（<a href="../../posts/case-first-agent-team-review-workflow/">case-first workflow</a> 的 case fidelity reviewer 抓的數據）：</p>
<table>
  <thead>
      <tr>
          <th>模組</th>
          <th>主要 case 類型</th>
          <th>Case fidelity</th>
          <th>主要失分類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>backend/02 cache</td>
          <td>Skeleton</td>
          <td>78%</td>
          <td>三層 cache latency 編造、ops/sec 編造</td>
      </tr>
      <tr>
          <td>backend/03 msgq</td>
          <td>Skeleton</td>
          <td>70%（最低）</td>
          <td>三方向擴寫成「4 個誤配場景」</td>
      </tr>
      <tr>
          <td>backend/04 obs</td>
          <td>Skeleton</td>
          <td>92.9%（最高）</td>
          <td>嚴守「揭露方向、通用補充」紀律</td>
      </tr>
      <tr>
          <td>backend/05 deploy</td>
          <td>Skeleton + Rich</td>
          <td>80%</td>
          <td>Rich case 判讀層被當 fact 引用</td>
      </tr>
      <tr>
          <td>backend/06 reliability</td>
          <td>Medium 全套</td>
          <td>88%</td>
          <td>Medium case 實作層擴寫過頭</td>
      </tr>
      <tr>
          <td>backend/07 batch 1</td>
          <td>Medium + Skeleton</td>
          <td>81%</td>
          <td>跨 case 合成 frame 升級成 case 揭露</td>
      </tr>
  </tbody>
</table>
<p>最高的 92.9% 跟最低的 70% 差 23 個百分點 — 關鍵變數是引用紀律、案例品質本身只佔次要因素。Skeleton case 嚴守「揭露方向」、不擴寫成 rich case 樣式、就能達到 90%+。</p>
<hr>
<h2 id="三類-case-的判讀條件">三類 case 的判讀條件</h2>
<h3 id="skeleton-case揭露-x-方向通用補充">Skeleton case：「揭露 X 方向、通用補充」</h3>
<p>典型：模組內部 N.Cx 案例庫中只有 frame、無具體數字的短篇 case。內容深度 10-30 行、給「議題」「視角」、不給「實作細節」。</p>
<p>承接紀律：</p>
<ul>
<li>引用為「揭露 X 方向」、不引用為「揭露 N 個具體場景數量」</li>
<li>後面補「以下展開基於通用工程知識補充」明示分層</li>
<li>不為了「整齊的 4 個攻擊面」「3 個攻擊向量」這種數字感、把 case 沒寫的 taxonomy 寫成 case 揭露</li>
</ul>
<p>例：Meta Cache Consistency case 只給「promotion、shard move、故障恢復」三個方向 → 引用為「揭露三個方向」、不引用為「揭露具體 inconsistency window 數字」。</p>
<h3 id="medium-case揭露-n-個機制--abc">Medium case：「揭露 N 個機制 — A、B、C」</h3>
<p>典型：模組內部 case 庫中、含結構化「決策機制」+「可觀測訊號」表、但無具體數字的中篇 case。內容深度 30-50 行、含 mechanism 名稱 + 訊號名稱、但不給 RPS / 延遲數字。</p>
<p>承接紀律：</p>
<ul>
<li>用 case 直接列出的 <em>mechanism 名稱</em> 精準引用、比 skeleton 精準、比 rich 保守</li>
<li>不擴寫到 case 沒提的具體實作層（會踩「實作層擴寫過頭」失分）</li>
<li>「決策機制」段通常是 fact 層、「常見陷阱」段可能含作者判讀層、引用時也要分層</li>
</ul>
<p>例：Amazon Shuffle Sharding case 揭露 cell boundary / shuffle sharding / static stability / constant work 四機制 → 引用四機制名稱、但不擴寫到「具體 shard 數量」「具體 cell 大小」等 case 沒提的細節。</p>
<h3 id="rich-case揭露具體-x-數字--設計">Rich case：「揭露具體 X 數字 / 設計」</h3>
<p>典型：跨模組 case 庫中含具體數字、設計細節、遷移路徑的長篇 case。內容深度 50-200 行、含具體 fact + 引用源。</p>
<p>承接紀律：</p>
<ul>
<li>可直接引用為事實、case 揭露的具體數字（RPS、延遲、TPS、stale window）可放進章節</li>
<li>但 rich case 內常含「觀察層」（具體 fact）跟「判讀層」（作者推論）、引用時要分層（見 <a href="../fact-vs-derive-citation-layering/">#116 fact-vs-derive 分層引用</a>）</li>
<li>引用判讀層時用「揭露 X 觀察 + 作者判讀 Y」明示分層、避免把推論寫成 fact</li>
</ul>
<p>例：Amazon Ads case「90M RPS + 5M writes/sec + 99.999%」 → 可直接寫進 KV 章節作為 reference。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Skeleton case 引用寫成「揭露 4 個具體場景」</td>
          <td>編造 case 沒寫的 taxonomy、reviewer B 抓 critical</td>
      </tr>
      <tr>
          <td>Skeleton case 引用補具體數字（從通用知識補進去）</td>
          <td>「Tubi 三層 cache L1 &lt; 1ms / L2 &lt; 10ms / L3 10-100ms」這類編造數字</td>
      </tr>
      <tr>
          <td>Medium case 引用擴寫到 case 沒提的實作細節</td>
          <td>「具體 shard 數量」「具體 partition key 數量」這類 over-extrapolation</td>
      </tr>
      <tr>
          <td>Rich case 引用合併觀察 + 判讀層</td>
          <td>「揭露 35ms latency 反推 region 部署」（35ms 是觀察、reasoning 是判讀）</td>
      </tr>
      <tr>
          <td>引用時不標 case 類型、寫稿時憑感覺承接深度</td>
          <td>跨章累積失分、reviewer 抓出後修正成本高</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="stage-1-抽-findings-時的判讀步驟">Stage 1 抽 findings 時的判讀步驟</h2>
<p>寫教學內容前、stage 1 audit case 庫時要 <em>標明 case 類型</em>：</p>
<ol>
<li>看 case 行數 + 內容密度、初判類型</li>
<li>看是否有具體數字 / 設計細節、確認 Rich case</li>
<li>看是否只給方向 / 議題、確認 Skeleton case</li>
<li>介於中間時、傾向保守判讀為 Skeleton（避免過度承接）</li>
<li>把類型寫進 findings 列表、stage 2 寫作時依類型決定承接深度</li>
</ol>
<p>跨類型混合引用：</p>
<ul>
<li>同一段內若引兩類 case、先寫 rich case fact 作為支撐、再用 skeleton case 補方向</li>
<li>不要把 skeleton case 的方向跟 rich case 的數字混合成單一斷言</li>
<li>跨類型引用時 disclaimer 要明示哪段屬通用、哪段屬 case fact</li>
</ul>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../fact-vs-derive-citation-layering/">#116 Fact vs Derive 分層引用</a></td>
          <td>case 內部 fact / derive 分層 — 本卡看 case 整體類型、#116 看 case 內部結構</td>
      </tr>
      <tr>
          <td><a href="../cross-case-synthesized-frame-must-be-labeled/">#117 跨 case 合成 frame 必須標明</a></td>
          <td>第三類失分 — 章節抽象 frame 不能升級成 case 揭露</td>
      </tr>
      <tr>
          <td><a href="../security-citation-currency-and-precision/">#104 security citation 時效精確</a></td>
          <td>Citation 三大 surface 中的 standard surface — 本卡跟 #116/#117 是 case citation surface、三者並列為 citation 紀律的不同 surface</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>case fidelity 是輪 5（反例 / 邊界）+ stakes 軸（輪 E.5 citation）的具體實作</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>同骨 pattern 在 case 引用 surface 的展現 — 便利的引用句型（補通用知識、不分層）跟 case fidelity 反向</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 Single Source of Truth</a></td>
          <td>同骨 pattern 在不同 surface 的展現 — #44 處理 engineering value 的住址、本卡處理 narrative attribution 的住址</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫到「揭露 N 個」時不確定 N 是不是 case 真的列了</td>
          <td>回 case 原文 grep、不確定就降級為「揭露 X 方向」</td>
      </tr>
      <tr>
          <td>Skeleton case 引用突然出現具體數字（從 LLM 記憶湧現）</td>
          <td>數字幾乎一定是編造、立即刪掉或標 disclaimer</td>
      </tr>
      <tr>
          <td>Rich case 引用句含「才是 / 必須 / 一定 / 關鍵是」</td>
          <td>通常是把作者判讀升級成 fact、退回 case 原文找條件性表述</td>
      </tr>
      <tr>
          <td>同段引用兩類 case 但語氣一致</td>
          <td>分層遺失、重寫成「rich case 補 fact + skeleton case 補方向」</td>
      </tr>
      <tr>
          <td>Findings 列表沒標 case 類型</td>
          <td>Stage 1 紀律失效、回 case 補類型再寫</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：誤判 case 類型 = 引用深度錯位 = case fidelity 失分。Stage 1 抽 findings 時花 30 秒判類型、能省 stage 4 修正 5-10 分鐘 / 案例。</p>
]]></content:encoded></item><item><title>引用案例要分觀察層 / 判讀層、強化詞是錯位訊號</title><link>https://tarrragon.github.io/blog/report/fact-vs-derive-citation-layering/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/fact-vs-derive-citation-layering/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Fact vs Derive 分層是 rich case 引用的內部紀律：case 內容分成觀察層（具體事實）跟判讀層（作者推論）兩層、章節引用時要各自標明來源、避免把作者判讀升級為 case fact。具體分層：&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>觀察層（Fact）&lt;/td>
 &lt;td>case 直接寫的具體事實、數字、設計細節&lt;/td>
 &lt;td>直接引用為事實、可放章節作為支撐&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>判讀層（Derive）&lt;/td>
 &lt;td>case 作者的推論段、「我們判讀」「這意味著」「關鍵是」「核心是」「才是」等詞引出&lt;/td>
 &lt;td>用「作者判讀」「（case 中 X 屬作者推論層、本章引用此推論）」明示分層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩層在章節引用時要分層標明 — 一旦把判讀升級為 fact、章節就失去 case 支撐、讀者回查 case 時會發現章節說的「揭露」實際是作者推論。&lt;/p>
&lt;hr>
&lt;h2 id="跟case-類型決定引用深度的差別">跟「Case 類型決定引用深度」的差別&lt;/h2>
&lt;p>&lt;a href="../case-type-graded-citation-depth/">#115 case 類型決定引用深度&lt;/a> 看 case 整體類型（skeleton / medium / rich）、決定承接深度。本卡看 case 內部結構（觀察 vs 判讀）、決定引用時要不要分層。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>#115 case 類型決定引用深度&lt;/th>
 &lt;th>#116 fact vs derive 分層（本卡）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>看什麼&lt;/td>
 &lt;td>case 整體（行數 + 內容密度）&lt;/td>
 &lt;td>case 內部結構（觀察段 vs 判讀段）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要風險&lt;/td>
 &lt;td>Skeleton 擴寫成 rich case（編造數字）&lt;/td>
 &lt;td>Rich case 內判讀層被當 fact 引用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>應用範圍&lt;/td>
 &lt;td>所有 case 引用都要先判類型&lt;/td>
 &lt;td>主要適用 rich case + medium case 的判讀段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對應的失分類型&lt;/td>
 &lt;td>over-extrapolation（編造）&lt;/td>
 &lt;td>fact-derive 錯位（強化詞 / 漏條件）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者互補：&lt;/p>
&lt;ul>
&lt;li>Skeleton case：主要是前者風險（擴寫成 fact）&lt;/li>
&lt;li>Rich case：兩個風險都要防（先判類型、再分層引用）&lt;/li>
&lt;li>Medium case：前者風險低（mechanism 名稱明確）、後者風險中（「常見陷阱」段可能含判讀）&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="為什麼這層紀律重要">為什麼這層紀律重要&lt;/h2>
&lt;p>LLM 寫作引用 rich case 時、容易把兩層壓縮成「揭露 X」、把作者判讀升級為 case fact。讀者回查 case 時會發現章節說的「fact」實際是作者判讀、章節的論述失去 case 支撐。&lt;/p>
&lt;p>backend/05 deployment 模組驗證、case fidelity reviewer 抓出 4 個 high issue 都屬此類：&lt;/p>
&lt;h3 id="實證-1riot-games-35ms-延遲">實證 1：Riot Games 35ms 延遲&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Case 觀察段&lt;/strong>：「35ms 是競技遊戲（VALORANT、League）的可接受上限」&lt;/li>
&lt;li>&lt;strong>Case 判讀段&lt;/strong>：「從這個門檻反推：玩家所在 region 不能跨洲、需要區域 cluster」&lt;/li>
&lt;li>&lt;strong>章節（錯）&lt;/strong>：「揭露 35ms latency 反推 region 部署」← 合併兩層、把判讀寫成揭露&lt;/li>
&lt;li>&lt;strong>章節（對）&lt;/strong>：「揭露 35ms 延遲門檻 + Local Zones / Outposts 區域部署、可推得『延遲門檻反推 region 部署數量』」← 分層標明&lt;/li>
&lt;/ul>
&lt;h3 id="實證-2gcp-k8s-130k-scale">實證 2：GCP K8s 130K scale&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Case 觀察段&lt;/strong>：「control plane 極限取決於 storage backend」+「GCP 用 Spanner 替換 etcd」（兩個分開的點）&lt;/li>
&lt;li>&lt;strong>Case 判讀段&lt;/strong>：「把 storage 從瓶頸變成『showed no signs of not being able to support higher scales』」&lt;/li>
&lt;li>&lt;strong>章節（錯）&lt;/strong>：「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」← 把判讀升級成硬性結論、強化詞「才是」是訊號&lt;/li>
&lt;li>&lt;strong>章節（對）&lt;/strong>：「揭露 control plane vs data plane 容量規劃要分開、storage backend（GCP 用 Spanner 替代 etcd）是 K8s 規模極限的核心瓶頸層」← 保留條件性表述&lt;/li>
&lt;/ul>
&lt;h3 id="實證-3riot-games-漏歷史轉折">實證 3：Riot Games 漏歷史轉折&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Case 觀察段&lt;/strong>：「關鍵架構決策：從 multi-tenant cluster 模型改成 single-tenant per game」&lt;/li>
&lt;li>&lt;strong>章節（錯）&lt;/strong>：「揭露 single-tenant per game 的多 cluster 策略」← 漏掉轉折、只保留終態&lt;/li>
&lt;li>&lt;strong>章節（對）&lt;/strong>：「揭露架構決策從 multi-tenant cluster 改成 single-tenant per game」← 保留 case 揭露的關鍵歷史&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="引用句型對照">引用句型對照&lt;/h2>
&lt;h3 id="skeleton-case-引用">Skeleton 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">對應 [X.CN case-title]：揭露 X / Y / Z 三個方向（case 直接列出）；
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">以下展開基於通用工程知識補充。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="rich-case-引用單層純觀察">Rich 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">對應 [X.CN case-title]：揭露 X 具體數字 / 設計（case 觀察層）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="rich-case-引用含作者判讀">Rich 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">對應 [X.CN case-title]：揭露 X 觀察 + 作者判讀 Y（case 中 Y 屬判讀層、
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">本章引用此推論）。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="rich-case-引用避免硬性結論">Rich case 引用（避免硬性結論）&lt;/h3>
&lt;p>避免使用「才是 / 必須 / 一定 / 關鍵是」這類強化詞、保留 case 原文的條件性表述（「取決於」「核心瓶頸」「主要驅動」）。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>Fact vs Derive 分層是 rich case 引用的內部紀律：case 內容分成觀察層（具體事實）跟判讀層（作者推論）兩層、章節引用時要各自標明來源、避免把作者判讀升級為 case fact。具體分層：</p>
<table>
  <thead>
      <tr>
          <th>層別</th>
          <th>來源</th>
          <th>引用紀律</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觀察層（Fact）</td>
          <td>case 直接寫的具體事實、數字、設計細節</td>
          <td>直接引用為事實、可放章節作為支撐</td>
      </tr>
      <tr>
          <td>判讀層（Derive）</td>
          <td>case 作者的推論段、「我們判讀」「這意味著」「關鍵是」「核心是」「才是」等詞引出</td>
          <td>用「作者判讀」「（case 中 X 屬作者推論層、本章引用此推論）」明示分層</td>
      </tr>
  </tbody>
</table>
<p>兩層在章節引用時要分層標明 — 一旦把判讀升級為 fact、章節就失去 case 支撐、讀者回查 case 時會發現章節說的「揭露」實際是作者推論。</p>
<hr>
<h2 id="跟case-類型決定引用深度的差別">跟「Case 類型決定引用深度」的差別</h2>
<p><a href="../case-type-graded-citation-depth/">#115 case 類型決定引用深度</a> 看 case 整體類型（skeleton / medium / rich）、決定承接深度。本卡看 case 內部結構（觀察 vs 判讀）、決定引用時要不要分層。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>#115 case 類型決定引用深度</th>
          <th>#116 fact vs derive 分層（本卡）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>看什麼</td>
          <td>case 整體（行數 + 內容密度）</td>
          <td>case 內部結構（觀察段 vs 判讀段）</td>
      </tr>
      <tr>
          <td>主要風險</td>
          <td>Skeleton 擴寫成 rich case（編造數字）</td>
          <td>Rich case 內判讀層被當 fact 引用</td>
      </tr>
      <tr>
          <td>應用範圍</td>
          <td>所有 case 引用都要先判類型</td>
          <td>主要適用 rich case + medium case 的判讀段</td>
      </tr>
      <tr>
          <td>對應的失分類型</td>
          <td>over-extrapolation（編造）</td>
          <td>fact-derive 錯位（強化詞 / 漏條件）</td>
      </tr>
  </tbody>
</table>
<p>兩者互補：</p>
<ul>
<li>Skeleton case：主要是前者風險（擴寫成 fact）</li>
<li>Rich case：兩個風險都要防（先判類型、再分層引用）</li>
<li>Medium case：前者風險低（mechanism 名稱明確）、後者風險中（「常見陷阱」段可能含判讀）</li>
</ul>
<hr>
<h2 id="為什麼這層紀律重要">為什麼這層紀律重要</h2>
<p>LLM 寫作引用 rich case 時、容易把兩層壓縮成「揭露 X」、把作者判讀升級為 case fact。讀者回查 case 時會發現章節說的「fact」實際是作者判讀、章節的論述失去 case 支撐。</p>
<p>backend/05 deployment 模組驗證、case fidelity reviewer 抓出 4 個 high issue 都屬此類：</p>
<h3 id="實證-1riot-games-35ms-延遲">實證 1：Riot Games 35ms 延遲</h3>
<ul>
<li><strong>Case 觀察段</strong>：「35ms 是競技遊戲（VALORANT、League）的可接受上限」</li>
<li><strong>Case 判讀段</strong>：「從這個門檻反推：玩家所在 region 不能跨洲、需要區域 cluster」</li>
<li><strong>章節（錯）</strong>：「揭露 35ms latency 反推 region 部署」← 合併兩層、把判讀寫成揭露</li>
<li><strong>章節（對）</strong>：「揭露 35ms 延遲門檻 + Local Zones / Outposts 區域部署、可推得『延遲門檻反推 region 部署數量』」← 分層標明</li>
</ul>
<h3 id="實證-2gcp-k8s-130k-scale">實證 2：GCP K8s 130K scale</h3>
<ul>
<li><strong>Case 觀察段</strong>：「control plane 極限取決於 storage backend」+「GCP 用 Spanner 替換 etcd」（兩個分開的點）</li>
<li><strong>Case 判讀段</strong>：「把 storage 從瓶頸變成『showed no signs of not being able to support higher scales』」</li>
<li><strong>章節（錯）</strong>：「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」← 把判讀升級成硬性結論、強化詞「才是」是訊號</li>
<li><strong>章節（對）</strong>：「揭露 control plane vs data plane 容量規劃要分開、storage backend（GCP 用 Spanner 替代 etcd）是 K8s 規模極限的核心瓶頸層」← 保留條件性表述</li>
</ul>
<h3 id="實證-3riot-games-漏歷史轉折">實證 3：Riot Games 漏歷史轉折</h3>
<ul>
<li><strong>Case 觀察段</strong>：「關鍵架構決策：從 multi-tenant cluster 模型改成 single-tenant per game」</li>
<li><strong>章節（錯）</strong>：「揭露 single-tenant per game 的多 cluster 策略」← 漏掉轉折、只保留終態</li>
<li><strong>章節（對）</strong>：「揭露架構決策從 multi-tenant cluster 改成 single-tenant per game」← 保留 case 揭露的關鍵歷史</li>
</ul>
<hr>
<h2 id="引用句型對照">引用句型對照</h2>
<h3 id="skeleton-case-引用">Skeleton 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">對應 [X.CN case-title]：揭露 X / Y / Z 三個方向（case 直接列出）；
</span></span><span class="line"><span class="ln">2</span><span class="cl">以下展開基於通用工程知識補充。</span></span></code></pre></div><h3 id="rich-case-引用單層純觀察">Rich 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">對應 [X.CN case-title]：揭露 X 具體數字 / 設計（case 觀察層）。</span></span></code></pre></div><h3 id="rich-case-引用含作者判讀">Rich 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">對應 [X.CN case-title]：揭露 X 觀察 + 作者判讀 Y（case 中 Y 屬判讀層、
</span></span><span class="line"><span class="ln">2</span><span class="cl">本章引用此推論）。</span></span></code></pre></div><h3 id="rich-case-引用避免硬性結論">Rich case 引用（避免硬性結論）</h3>
<p>避免使用「才是 / 必須 / 一定 / 關鍵是」這類強化詞、保留 case 原文的條件性表述（「取決於」「核心瓶頸」「主要驅動」）。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用句含「才是 / 必須 / 一定 / 關鍵是 / 唯一」這類絕對詞</td>
          <td>通常是把作者判讀升級成 fact</td>
      </tr>
      <tr>
          <td>跨越兩段 case 內容（觀察 + 判讀）寫成單一斷言</td>
          <td>應分層、否則 reviewer B 抓 high issue</td>
      </tr>
      <tr>
          <td>引用後直接展開細節、沒給「以下基於通用工程知識補充」承接</td>
          <td>容易把通用知識掛到 case 名下</td>
      </tr>
      <tr>
          <td>漏掉 case 揭露的歷史轉折、只保留終態</td>
          <td>把 case 的「決策轉折」教訓抹平、讀者失去歷史 context</td>
      </tr>
      <tr>
          <td>Stage 1 抽 findings 不標來源層（觀察 / 判讀）</td>
          <td>Stage 2 寫作時無 mark、必踩 fact-derive 錯位</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="stage-1-抽-findings-的標明格式">Stage 1 抽 findings 的標明格式</h2>
<p>抽 findings 時、rich case 的 finding 要標明來源層：</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">Finding: 線性擴展是 OLTP 設計最高目標、coordinator 是傳統 OLTP 的擴展瓶頸
</span></span><span class="line"><span class="ln">2</span><span class="cl">來源: 9.C10 Spanner 案例
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 觀察層：「2 nodes → 45K reads/sec, 4 nodes → 90K reads/sec」段（case fact）
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 判讀層：作者「線性擴展是最高目標」是推論（case 中標為判讀）
</span></span><span class="line"><span class="ln">5</span><span class="cl">章節: 1.11 全球分散式 OLTP
</span></span><span class="line"><span class="ln">6</span><span class="cl">引用方式: 觀察層直接引用、判讀層用「作者判讀」明示</span></span></code></pre></div><p>Stage 2 寫作時依照 finding 列表的層別標記決定引用句型。</p>
<hr>
<h2 id="自掃描提示">自掃描提示</h2>
<p>寫作完後、檢查每處 rich case 引用是否：</p>
<ol>
<li>用了「才是 / 必須 / 一定 / 關鍵是 / 唯一」等強化詞 → 通常是把判讀升級成 fact</li>
<li>跨越兩段 case 內容（觀察 + 判讀）卻寫成單一斷言 → 應分層</li>
<li>引用後直接展開細節、沒給「以下基於通用工程知識補充」承接 → 容易把通用知識掛到 case 名下</li>
<li>漏掉 case 揭露的歷史轉折 / 條件 / 邊界 → 重讀 case「決策段」補回</li>
</ol>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../case-type-graded-citation-depth/">#115 案例引用深度跟著 case 類型走</a></td>
          <td>互補 — #115 看 case 整體類型、本卡看 case 內部結構</td>
      </tr>
      <tr>
          <td><a href="../cross-case-synthesized-frame-must-be-labeled/">#117 跨 case 合成 frame 必須標明</a></td>
          <td>第三類失分 — 章節 derive 升級成 case 揭露（07 新發現）</td>
      </tr>
      <tr>
          <td><a href="../security-citation-currency-and-precision/">#104 security citation 時效精確</a></td>
          <td>Citation 三大 surface 中的 standard surface — 本卡是 case citation surface、兩者並列為 citation 紀律的不同 surface（standard / internal-link / case）</td>
      </tr>
      <tr>
          <td><a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭稀釋技術精度</a></td>
          <td>強化詞屬「結局描述代替契約描述」、跟本卡的判讀層升級為 fact 同類</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>高 stakes 內容輪 E.5 citation 精確度檢查包含本卡紀律</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>同骨 pattern — 便利的引用句型（壓縮兩層成「揭露 X」、補強化詞）跟 case fidelity 紀律反向</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用句含強化詞（才是 / 必須 / 一定）</td>
          <td>回 case 原文確認是 fact 還是 derive、derive 要降級或標明</td>
      </tr>
      <tr>
          <td>找不到 case 原文的對應段</td>
          <td>引用是 LLM 推論、不是 case 揭露、退回「揭露 X 方向」</td>
      </tr>
      <tr>
          <td>Rich case 引用沒分層、整段平鋪</td>
          <td>用「揭露 X 觀察 + 作者判讀 Y」重寫</td>
      </tr>
      <tr>
          <td>章節 + 通用工程知識段沒明確分隔</td>
          <td>補「以下基於通用工程知識補充」承接</td>
      </tr>
      <tr>
          <td>Reviewer B 抓 high issue 集中在 rich case</td>
          <td>紀律失效、整章節重審所有 rich case 引用</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Fact 跟 derive 的差別在「來源是 case 還是作者」、跟內容對錯無關。讀者回查 case 時、要能反向 trace 章節的每個斷言到 case 原文的對應段、找不到的就是錯位。</p>
]]></content:encoded></item><item><title>跨多個 case 合成的 frame 必須標為章節合成、非 case 原文</title><link>https://tarrragon.github.io/blog/report/cross-case-synthesized-frame-must-be-labeled/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cross-case-synthesized-frame-must-be-labeled/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>跨 case 合成 frame 必須標為章節合成、非 case 原文揭露。這層紀律規範「章節把多個 case 的失效訊號抽象為更高層概念」時的引用語氣 — 抽象 frame 在 case 原文 grep 不到時、要 explicit 標「本章合成」、避免讀者把章節 derive 當 case fact。兩種寫法的差別：&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>Case 原文段直接寫此 frame&lt;/td>
 &lt;td>「兩 case 共同標明 X」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>章節從多個 case 失效訊號抽象&lt;/td>
 &lt;td>「本章把兩者抽象為 X 是 YYY 視角的合成 frame、非 case 原文框架」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩種寫法的差別在 case fidelity 紀律 — 不只是修辭層的選擇 — reviewer B 對照原文時、會抓「揭露 X」斷言是否有 case 原文支撐。&lt;/p>
&lt;hr>
&lt;h2 id="跟既有-fact-vs-derive-分層的差別">跟既有 fact-vs-derive 分層的差別&lt;/h2>
&lt;p>&lt;a href="../fact-vs-derive-citation-layering/">#116 fact vs derive 分層&lt;/a> 處理 &lt;em>單一 case 內部&lt;/em> 的觀察層 vs 判讀層分層。本卡處理 &lt;em>跨多個 case&lt;/em> 抽象出更高層 frame 的失分類型 — 屬於 fact-derive 紀律的第三類風險：&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>Skeleton 擴寫&lt;/td>
 &lt;td>case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露&lt;/td>
 &lt;td>case 說「異常查詢偵測維度」、章節寫「query 體積 1MB → 10GB / 天」（編造）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rich case fact-derive 混淆&lt;/td>
 &lt;td>case 有提、但屬作者判讀層的內容被寫成 case fact&lt;/td>
 &lt;td>case 把「35ms」放觀察、「反推 region 部署」放判讀；章節合併（升級判讀）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>跨 case 合成 frame&lt;/strong>（本卡）&lt;/td>
 &lt;td>case &lt;em>單獨&lt;/em> 寫的訊號被章節 &lt;em>跨 case 合成&lt;/em> 抽象為更高層 frame、frame 本身不在任一 case 原文&lt;/td>
 &lt;td>Uber 寫「告警串接不足」、Slack 寫「訊號未匯流」、章節合成「跨工具回查壓力」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="為什麼這層紀律重要">為什麼這層紀律重要&lt;/h2>
&lt;p>LLM 寫教學內容時容易把多個 case 的相似訊號抽象成 frame、讓段落結構更清楚。但 &lt;em>標明 frame 來源&lt;/em> 直接決定 case fidelity：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Case 真的揭露 frame&lt;/strong>：case 原文段直接寫此 frame、可寫「兩 case 共同標明 X」（屬合法 fact 引用）&lt;/li>
&lt;li>&lt;strong>章節從 case 失效訊號抽象&lt;/strong>：case 寫的是 &lt;em>單獨&lt;/em> 訊號、章節把多個訊號抽象成更高層 frame、要明示「本章合成、非 case 原文」&lt;/li>
&lt;/ul>
&lt;p>漏掉這層 disclaimer、讀者把章節 derive 當成 case fact、回查 case 時會找不到 frame、章節失去 case 支撐。&lt;/p>
&lt;hr>
&lt;h2 id="實證案例">實證案例&lt;/h2>
&lt;p>backend/07 batch 1 模組驗證、case fidelity reviewer 抓的 2 個 high issue 都屬此類：&lt;/p>
&lt;h3 id="實證-177-跨工具回查壓力">實證 1：7.7 跨工具回查壓力&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>章節（錯）&lt;/strong>：「對應 [Uber 2022] 跟 [Slack 2022]：兩個案例都揭露『身分事件後的跨工具回查壓力』」&lt;/li>
&lt;li>&lt;strong>Case 原文&lt;/strong>：Uber 失效控制面寫「身分異常事件與值班告警串接不足」、Slack 寫「程式碼資產存取異常訊號未快速匯流」— 都是 &lt;em>單工具內&lt;/em> 的訊號失效、「跨工具」這個 axis 是章節合成&lt;/li>
&lt;li>&lt;strong>章節（對）&lt;/strong>：「兩個案例分別在身分監控層揭露同類失效訊號 — Uber 標明 X、Slack 標明 Y。本章把兩者抽象為『跨工具回查壓力』是稽核視角的合成 frame、非 case 原文框架。」&lt;/li>
&lt;/ul>
&lt;h3 id="實證-277-平台責任切分">實證 2：7.7 平台責任切分&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>章節（錯）&lt;/strong>：「對應 [SolarWinds 2020]：揭露的『供應鏈事件中的平台責任切分』是稽核層的代表壓力場景」&lt;/li>
&lt;li>&lt;strong>Case 原文&lt;/strong>：失效控制面寫「更新來源信任過於單點」「行為監測難以區分合法元件」「供應鏈異常缺隔離流程」— 都是供應鏈信任議題、不是「平台 vs 產品的 audit 責任分離」&lt;/li>
&lt;li>&lt;strong>章節（對）&lt;/strong>：「案例的失效控制面標明 X / Y / Z。本章把這幾條失效面從供應鏈信任視角延伸到稽核視角、抽象為『平台 vs 產品的責任邊界判讀壓力』— 此 frame 為本章合成、非 case 原文。」&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="反例case-真的揭露-frame不需-disclaimer">反例：case 真的揭露 frame（不需 disclaimer）&lt;/h2>
&lt;p>跨 case 引用是否要標「本章合成」、取決於 frame 在 case 原文是否能 grep 找到。當 case 原文段直接寫此 frame、可直接引用：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>跨 case 合成 frame 必須標為章節合成、非 case 原文揭露。這層紀律規範「章節把多個 case 的失效訊號抽象為更高層概念」時的引用語氣 — 抽象 frame 在 case 原文 grep 不到時、要 explicit 標「本章合成」、避免讀者把章節 derive 當 case fact。兩種寫法的差別：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>引用紀律</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Case 原文段直接寫此 frame</td>
          <td>「兩 case 共同標明 X」</td>
      </tr>
      <tr>
          <td>章節從多個 case 失效訊號抽象</td>
          <td>「本章把兩者抽象為 X 是 YYY 視角的合成 frame、非 case 原文框架」</td>
      </tr>
  </tbody>
</table>
<p>兩種寫法的差別在 case fidelity 紀律 — 不只是修辭層的選擇 — reviewer B 對照原文時、會抓「揭露 X」斷言是否有 case 原文支撐。</p>
<hr>
<h2 id="跟既有-fact-vs-derive-分層的差別">跟既有 fact-vs-derive 分層的差別</h2>
<p><a href="../fact-vs-derive-citation-layering/">#116 fact vs derive 分層</a> 處理 <em>單一 case 內部</em> 的觀察層 vs 判讀層分層。本卡處理 <em>跨多個 case</em> 抽象出更高層 frame 的失分類型 — 屬於 fact-derive 紀律的第三類風險：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>風險</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Skeleton 擴寫</td>
          <td>case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露</td>
          <td>case 說「異常查詢偵測維度」、章節寫「query 體積 1MB → 10GB / 天」（編造）</td>
      </tr>
      <tr>
          <td>Rich case fact-derive 混淆</td>
          <td>case 有提、但屬作者判讀層的內容被寫成 case fact</td>
          <td>case 把「35ms」放觀察、「反推 region 部署」放判讀；章節合併（升級判讀）</td>
      </tr>
      <tr>
          <td><strong>跨 case 合成 frame</strong>（本卡）</td>
          <td>case <em>單獨</em> 寫的訊號被章節 <em>跨 case 合成</em> 抽象為更高層 frame、frame 本身不在任一 case 原文</td>
          <td>Uber 寫「告警串接不足」、Slack 寫「訊號未匯流」、章節合成「跨工具回查壓力」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="為什麼這層紀律重要">為什麼這層紀律重要</h2>
<p>LLM 寫教學內容時容易把多個 case 的相似訊號抽象成 frame、讓段落結構更清楚。但 <em>標明 frame 來源</em> 直接決定 case fidelity：</p>
<ul>
<li><strong>Case 真的揭露 frame</strong>：case 原文段直接寫此 frame、可寫「兩 case 共同標明 X」（屬合法 fact 引用）</li>
<li><strong>章節從 case 失效訊號抽象</strong>：case 寫的是 <em>單獨</em> 訊號、章節把多個訊號抽象成更高層 frame、要明示「本章合成、非 case 原文」</li>
</ul>
<p>漏掉這層 disclaimer、讀者把章節 derive 當成 case fact、回查 case 時會找不到 frame、章節失去 case 支撐。</p>
<hr>
<h2 id="實證案例">實證案例</h2>
<p>backend/07 batch 1 模組驗證、case fidelity reviewer 抓的 2 個 high issue 都屬此類：</p>
<h3 id="實證-177-跨工具回查壓力">實證 1：7.7 跨工具回查壓力</h3>
<ul>
<li><strong>章節（錯）</strong>：「對應 [Uber 2022] 跟 [Slack 2022]：兩個案例都揭露『身分事件後的跨工具回查壓力』」</li>
<li><strong>Case 原文</strong>：Uber 失效控制面寫「身分異常事件與值班告警串接不足」、Slack 寫「程式碼資產存取異常訊號未快速匯流」— 都是 <em>單工具內</em> 的訊號失效、「跨工具」這個 axis 是章節合成</li>
<li><strong>章節（對）</strong>：「兩個案例分別在身分監控層揭露同類失效訊號 — Uber 標明 X、Slack 標明 Y。本章把兩者抽象為『跨工具回查壓力』是稽核視角的合成 frame、非 case 原文框架。」</li>
</ul>
<h3 id="實證-277-平台責任切分">實證 2：7.7 平台責任切分</h3>
<ul>
<li><strong>章節（錯）</strong>：「對應 [SolarWinds 2020]：揭露的『供應鏈事件中的平台責任切分』是稽核層的代表壓力場景」</li>
<li><strong>Case 原文</strong>：失效控制面寫「更新來源信任過於單點」「行為監測難以區分合法元件」「供應鏈異常缺隔離流程」— 都是供應鏈信任議題、不是「平台 vs 產品的 audit 責任分離」</li>
<li><strong>章節（對）</strong>：「案例的失效控制面標明 X / Y / Z。本章把這幾條失效面從供應鏈信任視角延伸到稽核視角、抽象為『平台 vs 產品的責任邊界判讀壓力』— 此 frame 為本章合成、非 case 原文。」</li>
</ul>
<hr>
<h2 id="反例case-真的揭露-frame不需-disclaimer">反例：case 真的揭露 frame（不需 disclaimer）</h2>
<p>跨 case 引用是否要標「本章合成」、取決於 frame 在 case 原文是否能 grep 找到。當 case 原文段直接寫此 frame、可直接引用：</p>
<h3 id="反例-1邊界設備三同步-mechanism">反例 1：邊界設備三同步 mechanism</h3>
<ul>
<li><strong>章節</strong>：「對應 [Citrix Bleed 2023] 跟 [PAN-OS 2024]：兩個案例的『mechanism 總綱』段共同標明這個三同步原則」</li>
<li><strong>Case 原文</strong>：兩個 case 文末「mechanism 總綱」段確實寫「邊界事件的核心是讓『漏洞修補』『會話 / 憑證失效』『異常痕跡清查』三件事同步發生」</li>
<li><strong>判讀</strong>：frame 在 case 原文、可引用「兩 case 共同標明」、不需 disclaimer</li>
</ul>
<p>差別判斷：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該怎麼寫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Frame 文字在 case 原文 grep 找得到</td>
          <td>「兩 case 共同標明 X」</td>
      </tr>
      <tr>
          <td>Frame 是章節從 case 失效訊號抽象出</td>
          <td>「本章把 X 抽象為 Y 是 Z 視角的合成 frame」</td>
      </tr>
      <tr>
          <td>部分 case 揭露 frame、部分章節抽象</td>
          <td>兩段拆開、各自標明</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="為什麼-llm-容易踩">為什麼 LLM 容易踩</h2>
<p>從 LLM 寫教學內容的視角看、跨 case 合成 frame 是「自然湧現」的模式：</p>
<ol>
<li>LLM 讀完多個 case 後、會自動抽象出共通 pattern（這是 LLM 的訓練優勢）</li>
<li>寫章節時、章節結構需要 frame 把多個 case 組織起來（教學結構需求）</li>
<li>合成 frame 寫成「兩 case 都揭露 X」最順、不寫 disclaimer 最省字數</li>
<li>結果是 <em>frame 本身不在 case 原文</em>、但章節寫得像 case 揭露</li>
</ol>
<p>LLM 沒辦法 self-detect 這個盲點 — 因為從 LLM 視角、「合成」跟「揭露」在語意上很接近、需要對照 case 原文才能分辨。</p>
<hr>
<h2 id="防範路徑">防範路徑</h2>
<h3 id="stage-2-寫作時主動防範">Stage 2 寫作時主動防範</h3>
<p>每寫一個跨 case 合成 frame、跑「frame 在 case 原文 grep 得到嗎」檢查：</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">rg <span class="s2">&#34;&lt;frame 文字&gt;&#34;</span> &lt;<span class="k">case</span> file&gt;</span></span></code></pre></div><p>抓不到 → 用「本章合成、非 case 原文」disclaimer
抓得到 → 直接引用「兩 case 共同標明」</p>
<h3 id="stage-3-reviewer-b-prompt-補強">Stage 3 reviewer B prompt 補強</h3>
<p>設計 reviewer B prompt 時、要明示「跨 case 合成 frame 必須標為本章合成、非 case 原文」是 high 級 issue 抓取項。沒明示時、reviewer B 容易把這類問題降級為 medium、累積失分。</p>
<p>prompt 應包含：</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">特別檢查：當引用句說「兩個 case 都揭露 X」時、確認 X 是 case 原文寫的、
</span></span><span class="line"><span class="ln">2</span><span class="cl">還是章節跨 case 合成的。後者要在引用句明示「本章合成 / 非 case 原文框架」。</span></span></code></pre></div><hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「兩個案例都揭露 X」但 X 在原文 grep 不到</td>
          <td>章節 derive 升級成 case fact、reviewer B 抓 high issue</td>
      </tr>
      <tr>
          <td>跨 case 引用沒 disclaimer、直接寫「揭露」</td>
          <td>讀者回查 case 找不到對應、章節失去支撐</td>
      </tr>
      <tr>
          <td>Case 失效訊號是單獨 mechanism、章節抽象成上位 frame 但寫得像 case 揭露</td>
          <td>把「合成」包裝成「揭露」、案例驅動寫作的紀律失效</td>
      </tr>
      <tr>
          <td>Stage 3 reviewer B prompt 沒明示此類為 high</td>
          <td>reviewer 容易降級為 medium、累積失分不被優先處理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../case-type-graded-citation-depth/">#115 案例引用深度跟著 case 類型走</a></td>
          <td>上游卡 — 先判 case 類型、再判跨 case 合成 frame 是否成立</td>
      </tr>
      <tr>
          <td><a href="../fact-vs-derive-citation-layering/">#116 Fact vs Derive 分層引用</a></td>
          <td>同類紀律 — case 內部 fact-derive 分層的延伸、應用到跨 case 情境</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>輪 5（反例 / 邊界）跟輪 E.2（claim → evidence 推論鏈完整）的具體實作</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass frame 顆粒度盲點</a></td>
          <td>同類盲點 — 一個是同 reviewer 多輪 catch 同類錯、本卡是跨 case 合成的章節盲點</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>同骨 pattern — 合成 frame 寫成「兩 case 都揭露 X」最順、不寫 disclaimer 最省字數、便利跟 fidelity 反向</td>
      </tr>
      <tr>
          <td><a href="../security-citation-currency-and-precision/">#104 security citation 時效精確</a></td>
          <td>Citation 三大 surface 中的 standard surface — 本卡跟 #116 是 case citation surface、三者並列為 citation 紀律的不同 surface</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>引用句說「兩個 case 都揭露 X」</td>
          <td>grep case 原文、X 沒寫的話補「本章合成」disclaimer</td>
      </tr>
      <tr>
          <td>Frame 寫得很順但 case 原文沒這個詞</td>
          <td>章節 derive、改成「本章把 X 抽象為 Y」</td>
      </tr>
      <tr>
          <td>Reviewer B 抓 high issue 集中在「跨 case 引用」</td>
          <td>紀律失效、整章節重審跨 case 引用</td>
      </tr>
      <tr>
          <td>寫多 case 比較時想用「兩個都揭露 X」結構</td>
          <td>先 grep 確認、抓不到的話改用「兩個分別揭露 X1 / X2、本章合成 Y」</td>
      </tr>
      <tr>
          <td>Case 是 medium / rich 類型但「揭露 frame」是抽象詞</td>
          <td>通常是合成、不是 case 原文 frame</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：跨 case 合成 frame 本身是合法的寫作技巧、問題在 <em>不標明</em>。一句 disclaimer（「本章合成、非 case 原文」）就能把 fact-derive 紀律補回來、修法成本極低。</p>
]]></content:encoded></item><item><title>Standard-driven 取代 Case-driven 適用 standard framework 比 case 庫成熟的領域</title><link>https://tarrragon.github.io/blog/report/standard-driven-vs-case-driven-domain-judgment/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/standard-driven-vs-case-driven-domain-judgment/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Standard-driven 是 case-first 的替代策略、適用 standard framework 比 case 庫成熟的領域（如 LLM 安全）。判斷該用哪種策略看四維度：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Case-driven 適用&lt;/th>
 &lt;th>Standard-driven 適用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>議題穩定度&lt;/td>
 &lt;td>高（5+ 年穩定）&lt;/td>
 &lt;td>低（&amp;lt; 1 年快速演進）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Case 公開度&lt;/td>
 &lt;td>高（充分的事故公告）&lt;/td>
 &lt;td>中或低（vendor disclosure 偏 marketing）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Standard 成熟度&lt;/td>
 &lt;td>中（多用 case 而非 standard）&lt;/td>
 &lt;td>高（standard framework 已成型）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維護半衰期&lt;/td>
 &lt;td>長&lt;/td>
 &lt;td>短（6 個月過時）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>典型對照&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;em>Case-driven 領域&lt;/em>：分散式系統 / 安全控制面 / 可靠性 / 訊息佇列（案例公開充分、半衰期 5+ 年）&lt;/li>
&lt;li>&lt;em>Standard-driven 領域&lt;/em>：LLM 安全（OWASP LLM Top 10 / MITRE ATLAS 已成型、案例 6 個月過時）、新興 compliance（NIST AI RMF）、cloud-native 標準（CNCF baseline）&lt;/li>
&lt;/ul>
&lt;p>Standard-driven 跟 case-driven 是平行的選項、依領域特性選用 — 兩者各自有適用情境、沒有退化 / 進階關係。誤套會導致 case 庫過早建構（standard-driven 領域）或章節停在教科書級（case-driven 領域）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼這個-axis-重要">為什麼這個 axis 重要&lt;/h2>
&lt;p>之前的 case-first workflow 預設「沒 case 庫的新主題、要先建 case 庫」— 這暗示缺 case 庫一定要先補。LLM 安全章節驗證了 &lt;em>第三條路&lt;/em>：&lt;/p>
&lt;p>當該領域的 &lt;em>標準框架&lt;/em>（如 OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS）已涵蓋 threat 分類、且 case 維護半衰期短於 standard、章節應 &lt;em>用 standard-driven 取代 case-driven&lt;/em>。&lt;/p>
&lt;p>誤套的代價：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>誤套 case-driven 到 standard-driven 領域&lt;/strong>：建 case 庫 8-12 小時、6 個月後 case 過時、變成維護負擔&lt;/li>
&lt;li>&lt;strong>誤套 standard-driven 到 case-driven 領域&lt;/strong>：章節停留在標準引用、漏掉真實事故才會浮現的議題、scope 盲點&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="standard-driven-章節的寫作策略">Standard-driven 章節的寫作策略&lt;/h2>
&lt;p>當判讀領域屬 standard-driven、章節採以下策略：&lt;/p>
&lt;h3 id="1-章節對齊-standard-framework-分類">1. 章節對齊 standard framework 分類&lt;/h3>
&lt;p>用 framework 章節 ID 標明（如 OWASP LLM01 / NIST AI-1.1）取代「對應 [case] —」斷言。&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">本章的 threat scope 對應 OWASP LLM Top 10 LLM01（Prompt Injection）+
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">LLM02（Insecure Output Handling）、NIST AI RMF 1.0 MEASURE-2.7。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-加-last-reviewed-cadence">2. 加 Last reviewed cadence&lt;/h3>
&lt;p>每 quarter 重評估 standard 版本跟章節對應、寫進 frontmatter：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">date&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="ld">2026-05-12&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;backend&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;security&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">&amp;gt; Last reviewed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="ld">2026-05-12&lt;/span>&lt;span class="l">（對齊 OWASP LLM Top 10 2025）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3案例觸發參考段標明公開案例累積中值得追蹤的方向">3.「案例觸發參考」段標明「公開案例累積中、值得追蹤的方向」&lt;/h3>
&lt;p>不寫「對應 [case] 揭露」斷言、避免引用源不穩定：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>Standard-driven 是 case-first 的替代策略、適用 standard framework 比 case 庫成熟的領域（如 LLM 安全）。判斷該用哪種策略看四維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Case-driven 適用</th>
          <th>Standard-driven 適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>議題穩定度</td>
          <td>高（5+ 年穩定）</td>
          <td>低（&lt; 1 年快速演進）</td>
      </tr>
      <tr>
          <td>Case 公開度</td>
          <td>高（充分的事故公告）</td>
          <td>中或低（vendor disclosure 偏 marketing）</td>
      </tr>
      <tr>
          <td>Standard 成熟度</td>
          <td>中（多用 case 而非 standard）</td>
          <td>高（standard framework 已成型）</td>
      </tr>
      <tr>
          <td>維護半衰期</td>
          <td>長</td>
          <td>短（6 個月過時）</td>
      </tr>
  </tbody>
</table>
<p><strong>典型對照</strong>：</p>
<ul>
<li><em>Case-driven 領域</em>：分散式系統 / 安全控制面 / 可靠性 / 訊息佇列（案例公開充分、半衰期 5+ 年）</li>
<li><em>Standard-driven 領域</em>：LLM 安全（OWASP LLM Top 10 / MITRE ATLAS 已成型、案例 6 個月過時）、新興 compliance（NIST AI RMF）、cloud-native 標準（CNCF baseline）</li>
</ul>
<p>Standard-driven 跟 case-driven 是平行的選項、依領域特性選用 — 兩者各自有適用情境、沒有退化 / 進階關係。誤套會導致 case 庫過早建構（standard-driven 領域）或章節停在教科書級（case-driven 領域）。</p>
<hr>
<h2 id="為什麼這個-axis-重要">為什麼這個 axis 重要</h2>
<p>之前的 case-first workflow 預設「沒 case 庫的新主題、要先建 case 庫」— 這暗示缺 case 庫一定要先補。LLM 安全章節驗證了 <em>第三條路</em>：</p>
<p>當該領域的 <em>標準框架</em>（如 OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS）已涵蓋 threat 分類、且 case 維護半衰期短於 standard、章節應 <em>用 standard-driven 取代 case-driven</em>。</p>
<p>誤套的代價：</p>
<ul>
<li><strong>誤套 case-driven 到 standard-driven 領域</strong>：建 case 庫 8-12 小時、6 個月後 case 過時、變成維護負擔</li>
<li><strong>誤套 standard-driven 到 case-driven 領域</strong>：章節停留在標準引用、漏掉真實事故才會浮現的議題、scope 盲點</li>
</ul>
<hr>
<h2 id="standard-driven-章節的寫作策略">Standard-driven 章節的寫作策略</h2>
<p>當判讀領域屬 standard-driven、章節採以下策略：</p>
<h3 id="1-章節對齊-standard-framework-分類">1. 章節對齊 standard framework 分類</h3>
<p>用 framework 章節 ID 標明（如 OWASP LLM01 / NIST AI-1.1）取代「對應 [case] —」斷言。</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">本章的 threat scope 對應 OWASP LLM Top 10 LLM01（Prompt Injection）+
</span></span><span class="line"><span class="ln">2</span><span class="cl">LLM02（Insecure Output Handling）、NIST AI RMF 1.0 MEASURE-2.7。</span></span></code></pre></div><h3 id="2-加-last-reviewed-cadence">2. 加 Last reviewed cadence</h3>
<p>每 quarter 重評估 standard 版本跟章節對應、寫進 frontmatter：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;...&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-12</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;...&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;backend&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;security&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;llm&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="nt">&gt; Last reviewed</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-12</span><span class="l">（對齊 OWASP LLM Top 10 2025）</span></span></span></code></pre></div><h3 id="3案例觸發參考段標明公開案例累積中值得追蹤的方向">3.「案例觸發參考」段標明「公開案例累積中、值得追蹤的方向」</h3>
<p>不寫「對應 [case] 揭露」斷言、避免引用源不穩定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gu">## 案例觸發參考
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">LLM agent prompt injection 的公開案例累積中、值得追蹤的方向：
</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="k">-</span> email assistant 場景：閱讀含 injection 的郵件、誘導 agent 觸發外送或洩漏
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">-</span> coding agent 場景：讀含 injection 的 PR / issue、誘導 agent 修改非預期檔案
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">-</span> 跨 agent chain：injection 在 sub-agent 累積、影響 parent agent 決策
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">&gt; </span><span class="ge">**事實查核註**：LLM agent prompt injection 是 2024-2025 快速演進的研究領域、
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="ge"></span><span class="k">&gt; </span><span class="ge">攻擊形態、防禦模式、公開案例都在累積中。建議引用前以 OWASP LLM Top 10、
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="ge"></span>&gt; 近期論文跟主流 vendor 的 incident 公告為準。</span></span></code></pre></div><h3 id="4-引用標準時用版本號">4. 引用標準時用版本號</h3>
<p>OWASP LLM Top 10 <strong>2025</strong> / NIST AI RMF <strong>1.0</strong> / MITRE ATLAS <strong>continuous</strong> — framework 改版要 trigger 章節重審。</p>
<p>引用源規範見 <a href="../security-citation-currency-and-precision/">#104 security citation 時效精確</a>。</p>
<hr>
<h2 id="何時要從-standard-driven-轉回-case-driven">何時要從 standard-driven 轉回 case-driven</h2>
<p>下列 tripwire 出現時、重新評估：</p>
<table>
  <thead>
      <tr>
          <th>Tripwire</th>
          <th>行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>該領域累積 5+ 個高可信度 case（vendor + academic + CVE 三來源交叉）</td>
          <td>補完整 case 庫、走 case-first workflow</td>
      </tr>
      <tr>
          <td>跨章 frame 重複出現、SSoT 衝突明顯</td>
          <td>case-driven mechanism 深化能解 SSoT 衝突</td>
      </tr>
      <tr>
          <td>出現「等級類似 SolarWinds」的 incident</td>
          <td>補單個 case、視為 high-impact reference</td>
      </tr>
      <tr>
          <td>讀者反饋章節太抽象、需要具體 case 才能理解 mechanism</td>
          <td>補 single high-impact case、不全建庫</td>
      </tr>
  </tbody>
</table>
<p>不滿足任一條件時、繼續走 standard-driven、不勉強建 case 庫。</p>
<hr>
<h2 id="07-llm-章節實證">07 LLM 章節實證</h2>
<p>backend/07 batch 2（LLM 安全 5 章）驗證 standard-driven 策略：</p>
<ul>
<li>章節 113-137 行、含完整 threat scope + 問題節點表 + 風險邊界</li>
<li>引用 OWASP LLM Top 10 + NIST AI RMF + MITRE ATLAS 取代個別 case 引用</li>
<li>加 <code>Last reviewed: 2026-05-12</code> cadence</li>
<li>「案例觸發參考」段寫「公開案例累積中、值得追蹤的方向」+「事實查核註」</li>
<li>完全不寫「對應 [case] —」斷言、不存在 case fidelity reviewer 該抓的準確性問題</li>
</ul>
<p>對照 backend/01-07 batch 1 的 case-driven 章節、LLM 章節是 <em>用不同方法達到同樣品質</em> — scope 涵蓋真實 production 議題（KV cache 跨租戶、shared prefix optimization、batch 推論順序敏感）、不停在教科書級內容。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>假設「沒 case 庫一定要先建」</td>
          <td>在 standard-driven 領域過早投入建 case 庫、6 個月後過時</td>
      </tr>
      <tr>
          <td>Standard-driven 章節沒加 Last reviewed cadence</td>
          <td>Standard 改版時章節未更新、引用變過時</td>
      </tr>
      <tr>
          <td>Standard-driven 章節寫「對應 [case] —」斷言</td>
          <td>引用源不穩定（vendor disclosure 偏 marketing）、case fidelity 風險高</td>
      </tr>
      <tr>
          <td>Case-driven 領域只用 framework 引用、不用案例</td>
          <td>漏掉真實事故議題、章節停在教科書級</td>
      </tr>
      <tr>
          <td>沒判讀領域類型、直接套 case-first workflow</td>
          <td>浪費 8-12 小時建 case 庫、得不到對應 ROI</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../case-type-graded-citation-depth/">#115 案例引用深度跟著 case 類型走</a></td>
          <td>Case-driven 適用時的 prerequisite — 先判 case 類型再決定承接深度</td>
      </tr>
      <tr>
          <td><a href="../routing-layer-chapter-recognition/">#119 章節已有 routing skeleton 走補強段</a></td>
          <td>本卡的下游 — 領域判為 case-driven 後、單章還要再判結構類型（routing layer / 空白 / 導讀）</td>
      </tr>
      <tr>
          <td><a href="../security-citation-currency-and-precision/">#104 security citation 時效精確</a></td>
          <td>Standard-driven 章節的 citation 紀律核心</td>
      </tr>
      <tr>
          <td><a href="../security-teaching-rigor-asymmetry/">#99 security teaching 嚴格度對應風險不對稱</a></td>
          <td>高 stakes 內容（含 LLM 安全）的審查標準</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>Standard-driven 章節跑輪 E（高 stakes）+ standard 版本對齊（輪 E.5）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>想寫某領域章節、找不到合適 case 庫</td>
          <td>先判四維度、可能該走 standard-driven、不是先建 case 庫</td>
      </tr>
      <tr>
          <td>Case 引用 6 個月後發現過時、要重寫</td>
          <td>領域屬 standard-driven、改用 framework + Last reviewed cadence</td>
      </tr>
      <tr>
          <td>Standard framework 改版（OWASP 出新版）</td>
          <td>章節 Last reviewed 重審、補對應 framework ID</td>
      </tr>
      <tr>
          <td>該領域累積 5+ 個高可信度 case</td>
          <td>Tripwire 觸發、考慮從 standard-driven 轉回 case-driven</td>
      </tr>
      <tr>
          <td>Vendor disclosure 多偏 marketing、case fidelity 低</td>
          <td>該領域 case 可信度不足、走 standard-driven 更穩定</td>
      </tr>
      <tr>
          <td>想引用 case 但找不到 academic / CVE 三來源交叉</td>
          <td>Case 公開度不足、改用 standard framework</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：寫教學內容的策略要依領域性質選擇 — case-driven 適合議題穩定 + case 公開的領域、standard-driven 適合 framework 比 case 庫成熟的領域。沒有預設策略。</p>
]]></content:encoded></item><item><title>章節已有 routing skeleton 走補強段、不空白擴章</title><link>https://tarrragon.github.io/blog/report/routing-layer-chapter-recognition/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/routing-layer-chapter-recognition/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Routing layer 章節辨識是擴章策略選擇的前置紀律。章節分三類、各對應不同擴章策略：&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>缺 threat scope / 問題節點表 / 風險邊界、structure 待建&lt;/td>
 &lt;td>走 case-driven 大幅擴章、建完整結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Routing layer 章節&lt;/td>
 &lt;td>已有 threat scope + 問題節點表 + 風險邊界 + 案例觸發段&lt;/td>
 &lt;td>走 &lt;em>補強段&lt;/em> 策略（在現有結構內補 mechanism 深化）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>導讀 / 標準引用章節&lt;/td>
 &lt;td>用 framework（OWASP / NIST）為主、案例為輔&lt;/td>
 &lt;td>走 standard-driven、加 Last reviewed cadence、不擴章&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>擴章策略要對應章節結構 — 在 routing layer 章節空白擴章會引發 frame 重複展開或章節失衡。誤判章節類型是 backend/07 batch 1 三個 H issue 的共同根因。&lt;/p>
&lt;hr>
&lt;h2 id="跟-standard-driven-領域判讀的差別">跟 standard-driven 領域判讀的差別&lt;/h2>
&lt;p>&lt;a href="../standard-driven-vs-case-driven-domain-judgment/">#118 standard-driven vs case-driven 領域判讀&lt;/a> 看 &lt;em>領域整體&lt;/em> 該用哪種策略。本卡看 &lt;em>單一章節&lt;/em> 的結構類型決定擴章策略。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>#118 領域判讀（領域級別）&lt;/th>
 &lt;th>#119 章節辨識（章節級別、本卡）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>看什麼&lt;/td>
 &lt;td>領域整體性質（議題穩定度 / standard 成熟度）&lt;/td>
 &lt;td>單一章節的結構（routing layer / 空白 / 標準引用）&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>領域判讀為 standard-driven 時、所有章節走 standard-driven&lt;/td>
 &lt;td>領域判讀為 case-driven 時、章節仍可能是 routing layer（走補強段）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者互補：&lt;/p>
&lt;ul>
&lt;li>領域 + 章節同 case-driven：走完整 case-first workflow（空白擴章）&lt;/li>
&lt;li>領域 case-driven + 章節 routing layer：走補強段（在現有結構內補深化）&lt;/li>
&lt;li>領域 standard-driven：所有章節用 standard 引用 + Last reviewed cadence&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="為什麼章節結構決定擴章策略">為什麼章節結構決定擴章策略&lt;/h2>
&lt;p>case-first workflow 之前預設「章節空白、case 庫驅動擴章」— 但實務中常遇到 &lt;em>章節已有 routing layer skeleton&lt;/em> 的情境（如 backend/07 batch 1 紅隊核心安全 7 章）：&lt;/p>
&lt;ul>
&lt;li>章節已有 &lt;em>threat scope&lt;/em>（In-scope / Out-of-scope 路由）&lt;/li>
&lt;li>已有 &lt;em>問題節點表&lt;/em>（4-6 個問題節點 + 判讀訊號 + 風險後果 + 前置控制面）&lt;/li>
&lt;li>已有 &lt;em>風險邊界&lt;/em>（4-6 條升級條件）&lt;/li>
&lt;li>已有 &lt;em>案例觸發參考&lt;/em>（已 link 3-5 個 case）&lt;/li>
&lt;/ul>
&lt;p>這種章節屬 &lt;em>routing layer&lt;/em> 結構 — 完整 skeleton 已存在、case 引用段已 link 既有 case。空白擴章會：&lt;/p>
&lt;ul>
&lt;li>跟既有問題節點表結構衝突&lt;/li>
&lt;li>把章節擴成厚重 case-driven 章節、失衡 routing 性質&lt;/li>
&lt;li>引發 frame 重複展開（既有節點 + 新擴章節點都寫一遍）&lt;/li>
&lt;/ul>
&lt;p>正確策略：&lt;em>補強段&lt;/em> — 在現有結構內補 mechanism 深化段、不重建結構。&lt;/p>
&lt;hr>
&lt;h2 id="routing-layer-章節的判讀訊號">Routing layer 章節的判讀訊號&lt;/h2>
&lt;p>掃描章節時、看以下訊號判斷是否為 routing layer：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>Routing layer 章節辨識是擴章策略選擇的前置紀律。章節分三類、各對應不同擴章策略：</p>
<table>
  <thead>
      <tr>
          <th>章節類型</th>
          <th>訊號</th>
          <th>擴章策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>空白章節</td>
          <td>缺 threat scope / 問題節點表 / 風險邊界、structure 待建</td>
          <td>走 case-driven 大幅擴章、建完整結構</td>
      </tr>
      <tr>
          <td>Routing layer 章節</td>
          <td>已有 threat scope + 問題節點表 + 風險邊界 + 案例觸發段</td>
          <td>走 <em>補強段</em> 策略（在現有結構內補 mechanism 深化）</td>
      </tr>
      <tr>
          <td>導讀 / 標準引用章節</td>
          <td>用 framework（OWASP / NIST）為主、案例為輔</td>
          <td>走 standard-driven、加 Last reviewed cadence、不擴章</td>
      </tr>
  </tbody>
</table>
<p>擴章策略要對應章節結構 — 在 routing layer 章節空白擴章會引發 frame 重複展開或章節失衡。誤判章節類型是 backend/07 batch 1 三個 H issue 的共同根因。</p>
<hr>
<h2 id="跟-standard-driven-領域判讀的差別">跟 standard-driven 領域判讀的差別</h2>
<p><a href="../standard-driven-vs-case-driven-domain-judgment/">#118 standard-driven vs case-driven 領域判讀</a> 看 <em>領域整體</em> 該用哪種策略。本卡看 <em>單一章節</em> 的結構類型決定擴章策略。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>#118 領域判讀（領域級別）</th>
          <th>#119 章節辨識（章節級別、本卡）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>看什麼</td>
          <td>領域整體性質（議題穩定度 / standard 成熟度）</td>
          <td>單一章節的結構（routing layer / 空白 / 標準引用）</td>
      </tr>
      <tr>
          <td>影響範圍</td>
          <td>整個模組的寫作策略</td>
          <td>單章的擴章方式</td>
      </tr>
      <tr>
          <td>互動關係</td>
          <td>領域判讀為 standard-driven 時、所有章節走 standard-driven</td>
          <td>領域判讀為 case-driven 時、章節仍可能是 routing layer（走補強段）</td>
      </tr>
  </tbody>
</table>
<p>兩者互補：</p>
<ul>
<li>領域 + 章節同 case-driven：走完整 case-first workflow（空白擴章）</li>
<li>領域 case-driven + 章節 routing layer：走補強段（在現有結構內補深化）</li>
<li>領域 standard-driven：所有章節用 standard 引用 + Last reviewed cadence</li>
</ul>
<hr>
<h2 id="為什麼章節結構決定擴章策略">為什麼章節結構決定擴章策略</h2>
<p>case-first workflow 之前預設「章節空白、case 庫驅動擴章」— 但實務中常遇到 <em>章節已有 routing layer skeleton</em> 的情境（如 backend/07 batch 1 紅隊核心安全 7 章）：</p>
<ul>
<li>章節已有 <em>threat scope</em>（In-scope / Out-of-scope 路由）</li>
<li>已有 <em>問題節點表</em>（4-6 個問題節點 + 判讀訊號 + 風險後果 + 前置控制面）</li>
<li>已有 <em>風險邊界</em>（4-6 條升級條件）</li>
<li>已有 <em>案例觸發參考</em>（已 link 3-5 個 case）</li>
</ul>
<p>這種章節屬 <em>routing layer</em> 結構 — 完整 skeleton 已存在、case 引用段已 link 既有 case。空白擴章會：</p>
<ul>
<li>跟既有問題節點表結構衝突</li>
<li>把章節擴成厚重 case-driven 章節、失衡 routing 性質</li>
<li>引發 frame 重複展開（既有節點 + 新擴章節點都寫一遍）</li>
</ul>
<p>正確策略：<em>補強段</em> — 在現有結構內補 mechanism 深化段、不重建結構。</p>
<hr>
<h2 id="routing-layer-章節的判讀訊號">Routing layer 章節的判讀訊號</h2>
<p>掃描章節時、看以下訊號判斷是否為 routing layer：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>屬 routing layer 章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>含「## 本章寫作邊界」段</td>
          <td>是</td>
      </tr>
      <tr>
          <td>含「## 本章 threat scope」段（In-scope / Out-of-scope）</td>
          <td>是</td>
      </tr>
      <tr>
          <td>含「## 從本章到實作」段（Mechanism + Delivery chain）</td>
          <td>是</td>
      </tr>
      <tr>
          <td>含「## 問題節點（案例觸發式）」表格</td>
          <td>是</td>
      </tr>
      <tr>
          <td>含「## 跨章議題交叉引用」段</td>
          <td>是</td>
      </tr>
      <tr>
          <td>含「## 常見風險邊界」段</td>
          <td>是</td>
      </tr>
      <tr>
          <td>含「## 案例觸發參考」段</td>
          <td>是</td>
      </tr>
      <tr>
          <td>含「## 下一步路由」段</td>
          <td>是</td>
      </tr>
      <tr>
          <td>章節行數 80-120 行（已有完整結構但不厚重）</td>
          <td>是</td>
      </tr>
  </tbody>
</table>
<p>含 4+ 個訊號 → 屬 routing layer 章節、走補強段策略。</p>
<hr>
<h2 id="補強段策略">補強段策略</h2>
<p>在 routing layer 章節內補 case-driven 深化段、遵守以下紀律：</p>
<h3 id="1-補強段位置">1. 補強段位置</h3>
<p>通常放在「問題節點表」後、「常見風險邊界」前：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 問題節點（案例觸發式）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">| 問題節點 | ... | ... |
</span></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="gu">## [新增補強段：對應某問題節點的 mechanism 深化]
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">[補強內容、case 引用三段式]
</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></code></pre></div><h3 id="2-補強段的範圍紀律">2. 補強段的範圍紀律</h3>
<ul>
<li>每個補強段對應 1-2 個既有問題節點、不擴新議題</li>
<li>不重建 threat scope / 問題節點表（保留 routing 性質）</li>
<li>補的 mechanism 深化要明示「本節聚焦 X 視角、canonical 在 Y 章」（避免 frame 重複）</li>
</ul>
<h3 id="3-cross-link-密度上升">3. Cross-link 密度上升</h3>
<p>補強段要明示「跟其他章節的視角分工」、否則 reviewer C 會抓 frame 重複展開：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 高權限工具的會話收斂節奏
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">身分被取得後、token 撤銷跟 session kill 的時間窗口直接決定攻擊者可觸及的
</span></span><span class="line"><span class="ln">4</span><span class="cl">資產面積、是初始落點橫向擴散的關鍵節流點。會話收斂節奏的 canonical 在
</span></span><span class="line"><span class="ln">5</span><span class="cl">[<span class="nt">7.5 § 會話重放跟全域失效</span>](<span class="na">../transport-trust-and-certificate-lifecycle/#會話重放跟全域失效canonical</span>)、
</span></span><span class="line"><span class="ln">6</span><span class="cl">本節從身分層補 token 撤銷窗口的 specific 訊號。
</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">對應 [Slack 2022 case]：...</span></span></code></pre></div><hr>
<h2 id="07-batch-1-實證">07 batch 1 實證</h2>
<p>backend/07 batch 1 七章節（identity-access / secrets / entrypoint / transport / credential-rotation / audit / workload-identity）走補強段策略：</p>
<ul>
<li>章節原本 80-100 行、補強後 100-140 行（+20-40 行 / 章）</li>
<li>每章補 2-3 個 mechanism 深化段、對應既有問題節點</li>
<li>三個 H issue（C-H1/H2/H3）都是 frame 重複展開、補強段紀律失效引起</li>
<li>修正後加 cross-link 明示「canonical 在 X 章、本節補 Y 視角」、frame 重複收斂</li>
</ul>
<p>對照 backend/06 reliability 模組（章節空白擴章）：</p>
<ul>
<li>章節原本 30-50 行、擴章後 80-90 行</li>
<li>每章建 mechanism + 訊號 + 反模式完整結構</li>
<li>沒有 routing skeleton 衝突問題</li>
</ul>
<p>兩種策略對應不同章節初始狀態、不互斥。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>在 routing layer 章節空白擴章、忽略既有問題節點表</td>
          <td>Frame 重複展開、章節失衡</td>
      </tr>
      <tr>
          <td>補強段沒明示「canonical 在 X 章、本節補 Y 視角」</td>
          <td>Reviewer C 抓 H issue、SSoT 不清</td>
      </tr>
      <tr>
          <td>補強段重建 threat scope / 問題節點表</td>
          <td>章節結構衝突、原 routing 性質被破壞</td>
      </tr>
      <tr>
          <td>沒先判章節類型、直接套 stage 2 寫作</td>
          <td>走錯策略、擴章失敗</td>
      </tr>
      <tr>
          <td>Routing layer 章節擴成厚重 case-driven 章節</td>
          <td>失衡 routing 性質、跨章導讀路徑斷掉</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../standard-driven-vs-case-driven-domain-judgment/">#118 Standard-driven vs Case-driven 領域判讀</a></td>
          <td>本卡的上游 — 先判讀領域為 case-driven、本卡才適用（standard-driven 領域走 standard 引用、不需章節結構判讀）</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 Single Source of Truth</a></td>
          <td>同骨 pattern 在不同 surface 的展現 — #44 處理 engineering value 的住址、本卡處理 narrative frame 的章節住址（補強段要明示「canonical 在 X 章」）</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>空白擴章比補強段便利、但便利會偏離意圖（routing 性質）</td>
      </tr>
      <tr>
          <td><a href="../case-type-graded-citation-depth/">#115 案例引用深度跟著 case 類型走</a></td>
          <td>補強段內 case 引用紀律的 prerequisite</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>輪 2（對意圖）的具體實作 — 寫補強段時要對齊章節原有 routing 意圖</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>章節已有完整 threat scope / 問題節點表 / 風險邊界</td>
          <td>走補強段、不空白擴章</td>
      </tr>
      <tr>
          <td>章節 80-120 行、結構完整但內容不厚重</td>
          <td>Routing layer 章節、補強段策略</td>
      </tr>
      <tr>
          <td>章節 30-50 行、缺結構</td>
          <td>空白章節、走 case-driven 大幅擴章</td>
      </tr>
      <tr>
          <td>Reviewer C 抓 frame 重複展開 H issue</td>
          <td>補強段紀律失效、補「canonical 在 X 章」cross-link</td>
      </tr>
      <tr>
          <td>章節擴章後失衡 routing 性質</td>
          <td>退回原章節、補強段重寫、保留 routing layer 結構</td>
      </tr>
      <tr>
          <td>想在 routing layer 章節重建 threat scope</td>
          <td>紀律失效訊號、改用補強段策略</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：擴章策略對應章節結構 — 空白章節走 case-driven 擴章、routing layer 章節走補強段、導讀章節走 standard-driven。三類策略各自有適用情境、選錯會引發 frame 重複展開或章節失衡。</p>
]]></content:encoded></item><item><title>案例引用三段式段落結構：概念定義 → case 引用 → 通用展開</title><link>https://tarrragon.github.io/blog/report/case-citation-three-part-structure/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/case-citation-three-part-structure/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Case 引用段落要走三段式結構紀律 — 段首是概念定義句、case 引用退到第二位置、最後通用工程知識展開。讓段落結構反映「概念 → 案例 → 操作」的論證流、不是「案例 → 概念 → 操作」的反向流。&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>「對應 [case] 揭露 X」段首取代核心概念&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第二位置&lt;/td>
 &lt;td>Case 引用：「對應 [case]：揭露 N 個機制 — &amp;hellip;」&lt;/td>
 &lt;td>跨章 13+ 段同句構、case 引用變儀式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>通用展開&lt;/td>
 &lt;td>「以下基於通用工程知識補充」+ 具體操作&lt;/td>
 &lt;td>通用知識直接掛在 case 名下、沒明示分層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>違反三段式最常見的形式是「概念定義句缺位、case 引用直接當段首」— 讀者尚未理解概念就被丟入案例細節。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他-case-引用紀律的差別">跟其他 case 引用紀律的差別&lt;/h2>
&lt;p>本卡跟 #115 / #116 / #117 是 case 引用紀律的不同 axis、互相正交：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>卡&lt;/th>
 &lt;th>Axis&lt;/th>
 &lt;th>看什麼&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>#115 案例引用深度跟著 case 類型走&lt;/td>
 &lt;td>引用「深度」&lt;/td>
 &lt;td>Case 整體類型（skeleton / medium / rich）決定承接深度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>#116 Fact vs Derive 分層引用&lt;/td>
 &lt;td>引用「分層」&lt;/td>
 &lt;td>Case 內部結構（觀察層 / 判讀層）決定要不要分層標明&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>#117 跨 case 合成 frame 必須標明&lt;/td>
 &lt;td>引用「合成」&lt;/td>
 &lt;td>跨多 case 抽象 frame 時要 explicit 標「本章合成」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>#120 案例引用三段式（本卡）&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>引用「結構」&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>段落順序：概念 → case → 通用&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四 axis 組合起來覆蓋 case 引用的完整紀律。寫每段 case 引用時、四個 axis 都要過一遍。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼這層紀律重要">為什麼這層紀律重要&lt;/h2>
&lt;p>backend/06 模組 reviewer 抓出 11/12 新段都犯「case 引用取代概念定義」的問題、屬最大宗 systemic 違規。原因：&lt;/p>
&lt;ol>
&lt;li>LLM 從 case 反推內容時、容易把 case 揭露當概念出發點&lt;/li>
&lt;li>Case 引用句構單一（「對應 [X]：揭露 N 個機制」）、跨章讀感同質&lt;/li>
&lt;li>概念定義被推到第二段、商業邏輯先於 case 的原則被推翻&lt;/li>
&lt;/ol>
&lt;p>三段式紀律的價值是把「概念」「案例」「展開」三層分離、讓讀者依層級理解。&lt;/p>
&lt;hr>
&lt;h2 id="三段式範例">三段式範例&lt;/h2>
&lt;h3 id="正確結構">正確結構&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="gu">## 失效局部化：cell 邊界跟 shuffle sharding
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">失效局部化是把單一依賴退化限制在最小可影響範圍的能力。把「依賴 budget」
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">從統一全域帳本拆成 per-cell 可用度結構、是這層治理的核心責任。失效局部
&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 class="nt">A1 Amazon Shuffle Sharding 與 Cell 邊界&lt;/span>](&lt;span class="na">.../shuffle-sharding-and-cell-boundary/&lt;/span>)：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">揭露四個機制對應上述四個子問題 — cell 邊界（擴散邊界）、shuffle sharding
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">（熱點重疊）、static stability（控制面解耦）、constant work（失敗模式工作
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">量恆定）。這四個機制把恢復策略從「全域搶救」轉為「分批收斂」。
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">以下基於通用工程知識補充的具體操作 ...&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="錯誤結構">錯誤結構&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="gu">## 失效局部化：cell 邊界跟 shuffle sharding
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">依賴 budget 的另一個面向是把失效影響限制在局部、不擴散到全域。多租戶
&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">對應 [A1 Amazon Shuffle Sharding 與 Cell 邊界]：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">揭露四個機制 — cell 邊界、shuffle sharding、static stability、constant work。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>差異：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>Case 引用段落要走三段式結構紀律 — 段首是概念定義句、case 引用退到第二位置、最後通用工程知識展開。讓段落結構反映「概念 → 案例 → 操作」的論證流、不是「案例 → 概念 → 操作」的反向流。</p>
<table>
  <thead>
      <tr>
          <th>段位</th>
          <th>內容</th>
          <th>反模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段首</td>
          <td>概念定義句：該概念是什麼、承擔什麼責任</td>
          <td>「對應 [case] 揭露 X」段首取代核心概念</td>
      </tr>
      <tr>
          <td>第二位置</td>
          <td>Case 引用：「對應 [case]：揭露 N 個機制 — &hellip;」</td>
          <td>跨章 13+ 段同句構、case 引用變儀式</td>
      </tr>
      <tr>
          <td>通用展開</td>
          <td>「以下基於通用工程知識補充」+ 具體操作</td>
          <td>通用知識直接掛在 case 名下、沒明示分層</td>
      </tr>
  </tbody>
</table>
<p>違反三段式最常見的形式是「概念定義句缺位、case 引用直接當段首」— 讀者尚未理解概念就被丟入案例細節。</p>
<hr>
<h2 id="跟其他-case-引用紀律的差別">跟其他 case 引用紀律的差別</h2>
<p>本卡跟 #115 / #116 / #117 是 case 引用紀律的不同 axis、互相正交：</p>
<table>
  <thead>
      <tr>
          <th>卡</th>
          <th>Axis</th>
          <th>看什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>#115 案例引用深度跟著 case 類型走</td>
          <td>引用「深度」</td>
          <td>Case 整體類型（skeleton / medium / rich）決定承接深度</td>
      </tr>
      <tr>
          <td>#116 Fact vs Derive 分層引用</td>
          <td>引用「分層」</td>
          <td>Case 內部結構（觀察層 / 判讀層）決定要不要分層標明</td>
      </tr>
      <tr>
          <td>#117 跨 case 合成 frame 必須標明</td>
          <td>引用「合成」</td>
          <td>跨多 case 抽象 frame 時要 explicit 標「本章合成」</td>
      </tr>
      <tr>
          <td><strong>#120 案例引用三段式（本卡）</strong></td>
          <td><strong>引用「結構」</strong></td>
          <td><strong>段落順序：概念 → case → 通用</strong></td>
      </tr>
  </tbody>
</table>
<p>四 axis 組合起來覆蓋 case 引用的完整紀律。寫每段 case 引用時、四個 axis 都要過一遍。</p>
<hr>
<h2 id="為什麼這層紀律重要">為什麼這層紀律重要</h2>
<p>backend/06 模組 reviewer 抓出 11/12 新段都犯「case 引用取代概念定義」的問題、屬最大宗 systemic 違規。原因：</p>
<ol>
<li>LLM 從 case 反推內容時、容易把 case 揭露當概念出發點</li>
<li>Case 引用句構單一（「對應 [X]：揭露 N 個機制」）、跨章讀感同質</li>
<li>概念定義被推到第二段、商業邏輯先於 case 的原則被推翻</li>
</ol>
<p>三段式紀律的價值是把「概念」「案例」「展開」三層分離、讓讀者依層級理解。</p>
<hr>
<h2 id="三段式範例">三段式範例</h2>
<h3 id="正確結構">正確結構</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="gu">## 失效局部化：cell 邊界跟 shuffle sharding
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">失效局部化是把單一依賴退化限制在最小可影響範圍的能力。把「依賴 budget」
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">從統一全域帳本拆成 per-cell 可用度結構、是這層治理的核心責任。失效局部
</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 class="nt">A1 Amazon Shuffle Sharding 與 Cell 邊界</span>](<span class="na">.../shuffle-sharding-and-cell-boundary/</span>)：
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">揭露四個機制對應上述四個子問題 — cell 邊界（擴散邊界）、shuffle sharding
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">（熱點重疊）、static stability（控制面解耦）、constant work（失敗模式工作
</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">以下基於通用工程知識補充的具體操作 ...</span></span></code></pre></div><h3 id="錯誤結構">錯誤結構</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="ln">1</span><span class="cl"><span class="gu">## 失效局部化：cell 邊界跟 shuffle sharding
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="gu"></span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">依賴 budget 的另一個面向是把失效影響限制在局部、不擴散到全域。多租戶
</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">對應 [A1 Amazon Shuffle Sharding 與 Cell 邊界]：
</span></span><span class="line"><span class="ln">7</span><span class="cl">揭露四個機制 — cell 邊界、shuffle sharding、static stability、constant work。</span></span></code></pre></div><p>差異：</p>
<ul>
<li><strong>正確</strong>：段首「失效局部化是&hellip;的能力」直接給概念定義、case 揭露的四機制對應到「四個子問題」、讀者懂概念才看到案例</li>
<li><strong>錯誤</strong>：段首用「另一個面向」鋪墊、case 直接列四機制、讀者尚未理解就被丟入案例細節</li>
</ul>
<hr>
<h2 id="案例引用句構分流07-模組強化">案例引用句構分流（07 模組強化）</h2>
<p>即使遵守三段式紀律、跨章 case 引用句構仍會同質化。07 batch 1 驗證 13 處 case 引用 11 處用同一句構「揭露 N 層失效控制面 — A、B、C」、讀者跨章連讀時把 case 引用當儀式而非論證。</p>
<p>分流原則：句構跟著 case 類型走、用 case 自身結構決定引用方式：</p>
<table>
  <thead>
      <tr>
          <th>Case 結構</th>
          <th>適用句構</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Case 直接列 N 個 mechanism</td>
          <td>「揭露 N 層失效控制面 — A、B、C」</td>
      </tr>
      <tr>
          <td>Case 主寫單一壓力場景</td>
          <td>「補的失效訊號是 X、mechanism 是 Y」</td>
      </tr>
      <tr>
          <td>Case 揭露歷史轉折</td>
          <td>「從 X 改成 Y 的關鍵架構決策」</td>
      </tr>
      <tr>
          <td>Case 揭露對比結構</td>
          <td>「揭露兩個層次的對照：A vs B」</td>
      </tr>
      <tr>
          <td>多 case 並列補不同層</td>
          <td>「A case 補 X、B case 補 Y」</td>
      </tr>
      <tr>
          <td>Case 揭露 mechanism 可引用範圍</td>
          <td>「案例『可落地檢查點』直接列出 mechanism 屬可引用範圍」</td>
      </tr>
  </tbody>
</table>
<p>寫多章時刻意變化句構、避免讀者連讀數章感「每段開頭都長一樣」。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段首直接是「對應 [case]」、沒概念定義句</td>
          <td>商業邏輯先於 case 原則被推翻、讀者尚未理解就被丟入案例細節</td>
      </tr>
      <tr>
          <td>段首用「另一個面向」「不只是 X、是 Y」鋪墊取代概念定義</td>
          <td>推進論證骨架取代概念先行</td>
      </tr>
      <tr>
          <td>三段式中段（case 引用）擴寫成具體實作細節</td>
          <td>把通用工程知識掛在 case 名下、case fidelity 失分</td>
      </tr>
      <tr>
          <td>通用展開段沒明示「以下基於通用工程知識補充」</td>
          <td>讀者誤以為展開內容也來自 case</td>
      </tr>
      <tr>
          <td>跨章 5+ 段用同一句構「揭露 N 層失效控制面」</td>
          <td>Case 引用變儀式、讀者連讀感同質</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="stage-2-自查清單">Stage 2 自查清單</h2>
<p>寫完每章 case 引用後、檢查：</p>
<ol>
<li><strong>段首是否是概念定義句</strong>？（不是 case 引用、不是「另一個面向」鋪墊）</li>
<li><strong>Case 引用是否在第二位置</strong>？（不是段首）</li>
<li><strong>通用展開是否有「以下基於通用工程知識補充」承接</strong>？</li>
<li><strong>句構是否跟前面章節相同</strong>？（同模組超過 3 章用同句構就該變化）</li>
</ol>
<p>掃描指令：</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"># 找段首是 case 引用的段（最嚴格）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rg -n <span class="s2">&#34;^對應 \[&#34;</span> &lt;module-paths&gt;
</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="c1"># 找 ## 標題後緊接 case 引用的段（要手動 review）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">rg -B0 -A3 -n <span class="s2">&#34;^## &#34;</span> &lt;file&gt; <span class="p">|</span> rg <span class="s2">&#34;對應 \[&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 找句構同質化（跨檔抓「揭露 N 層失效控制面」出現次數）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">rg -c <span class="s2">&#34;揭露[^。]*失效控制面&#34;</span> &lt;module-paths&gt;</span></span></code></pre></div><p><strong>False positive 警示</strong>：<code>^對應 \[</code> 在三段分離結構（概念定義段 → 空行 → case 引用獨立段）下會 false positive、要用 awk 看 prev line 是否為實質概念段：</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">awk <span class="s1">&#39;/^對應 \[/{prev_blank=(prev==&#34;&#34;); print FILENAME &#34;:&#34; NR &#34;: prev_blank=&#34; prev_blank} {prev=$0}&#39;</span> &lt;file&gt;</span></span></code></pre></div><p>prev_blank=1 + 前段有實質概念定義 → 屬規範允許（三段分離）。</p>
<hr>
<h2 id="polish-pass-修法">Polish pass 修法</h2>
<p>如果 stage 3 reviewer 抓出大量「case 引用段首」issue、polish pass 的修法：</p>
<ol>
<li>每個有 issue 的段、在 case 引用前補一句「概念定義 + 核心責任」</li>
<li>不重寫整段、只加 lead sentence（保留 case 引用本身）</li>
<li>變化 case 引用句構：把 11/12 段同一句構打散成 3-4 種變化</li>
<li>修完跑自掃描確認段首不再是 case 引用</li>
</ol>
<p>修法成本：每段補 1-2 句概念定義、單章約 5-10 分鐘、整模組 1-2 小時。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../case-type-graded-citation-depth/">#115 案例引用深度跟著 case 類型走</a></td>
          <td>互補 — 不同 axis、#115 看引用深度、本卡看引用結構</td>
      </tr>
      <tr>
          <td><a href="../fact-vs-derive-citation-layering/">#116 Fact vs Derive 分層引用</a></td>
          <td>互補 — 不同 axis、#116 看 case 內部分層、本卡看段落順序</td>
      </tr>
      <tr>
          <td><a href="../cross-case-synthesized-frame-must-be-labeled/">#117 跨 case 合成 frame 必須標明</a></td>
          <td>互補 — 不同 axis、#117 看跨 case 合成、本卡看單段結構</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>互補 — #83 是 review 流程跨輪 frame、本卡是寫作當下段落順序</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>同骨 pattern — 「對應 [case]」當段首最順、不寫概念定義最省字、便利跟意圖對齊反向</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點</a></td>
          <td>句構同質化是 #114 在 case 引用 surface 的具體展現</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段首是「對應 [case] 揭露 X」</td>
          <td>補概念定義句、case 引用退到第二位置</td>
      </tr>
      <tr>
          <td>段首用「另一個面向」「不只是 X、是 Y」鋪墊</td>
          <td>改成概念定義先行、不用對比骨架推進</td>
      </tr>
      <tr>
          <td>跨章 5+ 段用同句構「揭露 N 層失效控制面」</td>
          <td>Stage 5 polish pass 句構分流</td>
      </tr>
      <tr>
          <td>Reviewer A 抓出「case 引用段首」issue 多</td>
          <td>三段式紀律失效、整模組重審</td>
      </tr>
      <tr>
          <td>通用展開段沒明示「以下基於通用工程知識補充」</td>
          <td>補承接句、讓讀者知道展開內容是通用知識</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：三段式紀律的價值是把「概念 → 案例 → 操作」三層分離、讓讀者依層級理解。段首被 case 引用取代會推翻商業邏輯先於 case 的原則、是 LLM 寫教學內容的系統性傾向。</p>
]]></content:encoded></item><item><title>Agent team context 隔離設計：用不同 instance 換 frame、平行 background 保護主 context</title><link>https://tarrragon.github.io/blog/report/agent-team-context-isolation/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/agent-team-context-isolation/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Agent team context 隔離是 LLM-era review 工具設計的核心模式 — 用 N 個獨立 reviewer instance 各自跑 background、各自寫 output file、主 context 只接精煉摘要、不被 reviewer 細節污染。&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>Instance 隔離&lt;/td>
 &lt;td>N 個專責 reviewer 各自獨立 context&lt;/td>
 &lt;td>維度盲點分開處理、不互相干擾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Background 平行&lt;/td>
 &lt;td>不阻塞主 context、可同時跑 3-5 個 reviewer&lt;/td>
 &lt;td>時間從序列 30 分鐘縮到平行 10 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>輸出檔案隔離&lt;/td>
 &lt;td>Reviewer 寫 output file、不污染主 conversation&lt;/td>
 &lt;td>主 context 增量 ~3K token、節省 ~80% context&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主 context 只接摘要&lt;/td>
 &lt;td>Reviewer 完成後回傳精煉彙整&lt;/td>
 &lt;td>修正循環時 context 留給判讀、不被 raw issue 列表佔滿&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跟 multi-pass review（#83）的差別：#83 是 &lt;em>同一 reviewer 換輪次 frame&lt;/em>（生成 / 對意圖 / 機會成本 / grep / 反例）；本卡是 &lt;em>不同 reviewer instance 各自獨立&lt;/em>（規範 / 案例準確 / 跨章一致 / compositional-writing 等）。兩者正交、可疊加。&lt;/p>
&lt;hr>
&lt;h2 id="跟-multi-pass-review-的差別">跟 multi-pass review 的差別&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>#83 Multi-pass review&lt;/th>
 &lt;th>#121 Agent team context 隔離（本卡）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>軸定位&lt;/td>
 &lt;td>Frame 軸（一個 reviewer N 輪不同 frame）&lt;/td>
 &lt;td>Instance 軸（N 個 reviewer 各自獨立）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>解決問題&lt;/td>
 &lt;td>Working memory 限制（一輪 catch 不到所有層）&lt;/td>
 &lt;td>Context 污染（單一 reviewer context 被 raw input 佔滿）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適用對象&lt;/td>
 &lt;td>Author / 單一 reviewer 跑多輪&lt;/td>
 &lt;td>Agent team / 自動化平行 review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗模式&lt;/td>
 &lt;td>跳輪 → 某維度永遠做一半&lt;/td>
 &lt;td>Instance 數量不足 → 維度覆蓋不全&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩軸正交、可疊加 — 同一 reviewer instance 內跑 multi-pass（#83）、跨 reviewer instance 各自獨立（本卡）。Case-first stage 3 設計同時用兩軸：3 個 reviewer instance 各自獨立 + 每個 reviewer 內部跑多輪 frame check。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼這層設計重要">為什麼這層設計重要&lt;/h2>
&lt;p>單一 reviewer 同時處理多維度有兩個限制：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>維度盲點&lt;/strong>：一個 reviewer 同時看寫作規範 + 案例準確性 + 跨章一致性、容易維度互相干擾、最後每個維度都看不深&lt;/li>
&lt;li>&lt;strong>Context 污染&lt;/strong>：reviewer 讀完整 commit + 所有案例 + 所有章節後、自身 context 被佔滿、給的建議也對應主 context 跟著沉重&lt;/li>
&lt;/ol>
&lt;p>Context 隔離解這兩個問題：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>Agent team context 隔離是 LLM-era review 工具設計的核心模式 — 用 N 個獨立 reviewer instance 各自跑 background、各自寫 output file、主 context 只接精煉摘要、不被 reviewer 細節污染。</p>
<table>
  <thead>
      <tr>
          <th>設計面</th>
          <th>紀律</th>
          <th>效果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Instance 隔離</td>
          <td>N 個專責 reviewer 各自獨立 context</td>
          <td>維度盲點分開處理、不互相干擾</td>
      </tr>
      <tr>
          <td>Background 平行</td>
          <td>不阻塞主 context、可同時跑 3-5 個 reviewer</td>
          <td>時間從序列 30 分鐘縮到平行 10 分鐘</td>
      </tr>
      <tr>
          <td>輸出檔案隔離</td>
          <td>Reviewer 寫 output file、不污染主 conversation</td>
          <td>主 context 增量 ~3K token、節省 ~80% context</td>
      </tr>
      <tr>
          <td>主 context 只接摘要</td>
          <td>Reviewer 完成後回傳精煉彙整</td>
          <td>修正循環時 context 留給判讀、不被 raw issue 列表佔滿</td>
      </tr>
  </tbody>
</table>
<p>跟 multi-pass review（#83）的差別：#83 是 <em>同一 reviewer 換輪次 frame</em>（生成 / 對意圖 / 機會成本 / grep / 反例）；本卡是 <em>不同 reviewer instance 各自獨立</em>（規範 / 案例準確 / 跨章一致 / compositional-writing 等）。兩者正交、可疊加。</p>
<hr>
<h2 id="跟-multi-pass-review-的差別">跟 multi-pass review 的差別</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>#83 Multi-pass review</th>
          <th>#121 Agent team context 隔離（本卡）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>軸定位</td>
          <td>Frame 軸（一個 reviewer N 輪不同 frame）</td>
          <td>Instance 軸（N 個 reviewer 各自獨立）</td>
      </tr>
      <tr>
          <td>解決問題</td>
          <td>Working memory 限制（一輪 catch 不到所有層）</td>
          <td>Context 污染（單一 reviewer context 被 raw input 佔滿）</td>
      </tr>
      <tr>
          <td>適用對象</td>
          <td>Author / 單一 reviewer 跑多輪</td>
          <td>Agent team / 自動化平行 review</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>跳輪 → 某維度永遠做一半</td>
          <td>Instance 數量不足 → 維度覆蓋不全</td>
      </tr>
  </tbody>
</table>
<p>兩軸正交、可疊加 — 同一 reviewer instance 內跑 multi-pass（#83）、跨 reviewer instance 各自獨立（本卡）。Case-first stage 3 設計同時用兩軸：3 個 reviewer instance 各自獨立 + 每個 reviewer 內部跑多輪 frame check。</p>
<hr>
<h2 id="為什麼這層設計重要">為什麼這層設計重要</h2>
<p>單一 reviewer 同時處理多維度有兩個限制：</p>
<ol>
<li><strong>維度盲點</strong>：一個 reviewer 同時看寫作規範 + 案例準確性 + 跨章一致性、容易維度互相干擾、最後每個維度都看不深</li>
<li><strong>Context 污染</strong>：reviewer 讀完整 commit + 所有案例 + 所有章節後、自身 context 被佔滿、給的建議也對應主 context 跟著沉重</li>
</ol>
<p>Context 隔離解這兩個問題：</p>
<ul>
<li>用 N 個專責 reviewer、各自只處理一個維度 → 維度深度提升</li>
<li>Reviewer 各自 background、不污染主 context → 主 context 保留判讀空間</li>
<li>Reviewer 寫 output file、不傳 raw 內容到主 context → 主 context 增量極少</li>
</ul>
<hr>
<h2 id="設計紀律何時用幾個-reviewer">設計紀律：何時用幾個 reviewer</h2>
<p>Reviewer 數量決定取決於審查對象的維度複雜度：</p>
<table>
  <thead>
      <tr>
          <th>審查對象</th>
          <th>Reviewer 數</th>
          <th>維度分配</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Case-driven 章節擴章</td>
          <td>3 個</td>
          <td>A 寫作規範 / B 案例引用準確性 / C 跨章一致性</td>
      </tr>
      <tr>
          <td>方法論 / 自我審查</td>
          <td>4 個</td>
          <td>A 寫作規範 / B 三方自一致性 / C 概念邊界 / D compositional-writing 6 原則</td>
      </tr>
      <tr>
          <td>一般 PR review</td>
          <td>1-2 個</td>
          <td>規範 + correctness、不需要 case fidelity 維度</td>
      </tr>
      <tr>
          <td>高 stakes 內容（資安 / financial）</td>
          <td>4-5 個</td>
          <td>加 epistemic rigor reviewer（claim / evidence / threats）</td>
      </tr>
  </tbody>
</table>
<p>維度設計要對審查對象客製、不要固定一套維度套所有任務。</p>
<hr>
<h2 id="平行-background-的具體實作">平行 background 的具體實作</h2>
<p>Case-first stage 3 的實作 pattern：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Agent tool spawn 平行 background</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">for</span> <span class="n">reviewer_id</span> <span class="ow">in</span> <span class="p">[</span><span class="s1">&#39;A&#39;</span><span class="p">,</span> <span class="s1">&#39;B&#39;</span><span class="p">,</span> <span class="s1">&#39;C&#39;</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">Agent</span><span class="p">({</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">description</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;Reviewer </span><span class="si">{</span><span class="n">reviewer_id</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">dimension</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">subagent_type</span><span class="p">:</span> <span class="s2">&#34;general-purpose&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">run_in_background</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">prompt</span><span class="p">:</span> <span class="n">get_reviewer_prompt</span><span class="p">(</span><span class="n">reviewer_id</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"># 主 context 不阻塞、繼續其他工作</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># Reviewer 完成時主 context 接通知（task-notification）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># Reviewer 各自寫 output 到 /tmp/reviewer-{id}-report.md</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 主 context 讀 output 彙整、不讀 raw conversation transcript</span></span></span></code></pre></div><p>關鍵設計選擇：</p>
<ol>
<li><strong><code>run_in_background: true</code></strong>：平行跑、不阻塞</li>
<li><strong>Reviewer 寫 output file</strong>：報告寫 <code>/tmp/...</code> 不污染主 conversation</li>
<li><strong>主 context 不讀 reviewer transcript</strong>：只讀 task-notification 的 summary + 最後讀 output file</li>
<li><strong>Reviewer prompt 含「不要佔我主 context、報告寫進檔即可」明示</strong>：避免 reviewer 把 raw issue 都吐回主 conversation</li>
</ol>
<hr>
<h2 id="reviewer-維度設計跟著任務客製化">Reviewer 維度設計：跟著任務客製化</h2>
<p>Reviewer 維度不該固定 — backend/07 batch 1 案例驅動章節用「規範 / 案例 / 跨章一致」三維度、方法論審查用「規範 / 三方自一致 / 概念邊界 / compositional-writing」四維度。</p>
<p>設計原則：</p>
<ul>
<li><strong>拆 axis 不重疊</strong>：每個 reviewer 的維度跟其他 reviewer 互斥（如「規範」vs「案例準確性」是不同 axis）</li>
<li><strong>覆蓋審查對象的關鍵風險</strong>：審查 case-driven 章節要 case fidelity reviewer、審查方法論要三方自一致性 reviewer</li>
<li><strong>預期 issue baseline 設好</strong>：每 reviewer 給 prompt 預期數量、reviewer 不要過度抓 / 漏抓</li>
<li><strong>prompt 含主 context 保護指令</strong>：「報告寫到 /tmp/X-report.md、不要在主 conversation 吐 raw issue」</li>
</ul>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單一 reviewer 處理所有維度</td>
          <td>維度盲點 + context 污染、品質下降</td>
      </tr>
      <tr>
          <td>Reviewer 不寫 output file、直接在 conversation 吐 raw issue</td>
          <td>主 context 被 issue 列表佔滿、修正循環沒空間</td>
      </tr>
      <tr>
          <td>Reviewer 維度固定不變、套所有任務</td>
          <td>維度跟審查對象不對齊、漏抓關鍵風險</td>
      </tr>
      <tr>
          <td>Reviewer 不平行、序列跑</td>
          <td>時間成本高、序列 30 分鐘 vs 平行 10 分鐘</td>
      </tr>
      <tr>
          <td>Reviewer prompt 沒明示 baseline</td>
          <td>Reviewer 抓 5 個或 50 個都「完成」、無法判讀品質</td>
      </tr>
      <tr>
          <td>主 context 直接 Read reviewer transcript</td>
          <td>把 raw conversation 拉進主 context、context 污染</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>互補 — #83 是 frame 軸（一個 reviewer N 輪）、本卡是 instance 軸（N 個 reviewer 各自獨立）、兩軸正交可疊加</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點</a></td>
          <td>解同類問題的不同手法 — #114 用「keyword bank / reader simulation / self-criticism」三機制擴大覆蓋、本卡用 instance 隔離擴大覆蓋</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>同骨 pattern — 單一 reviewer 處理多維度最便利（不用 spawn / coordinate）、但意圖（深度 review）失準</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 兩次門檻</a></td>
          <td>跨機制 — 同 reviewer 多輪 catch 同類錯（#114）跟跨 reviewer instance（本卡）都是「換工具」的具體實作</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reviewer 給的建議「對應主 context 也沉重」</td>
          <td>Reviewer context 被污染、改 background instance 隔離</td>
      </tr>
      <tr>
          <td>主 context 修正循環時、不知道從哪個 issue 開始</td>
          <td>Reviewer 報告沒精煉、補 reviewer prompt 要求 summary 開頭</td>
      </tr>
      <tr>
          <td>多個 reviewer 抓到同類 issue</td>
          <td>維度設計重疊、調整 reviewer 維度分配</td>
      </tr>
      <tr>
          <td>Reviewer 序列跑、單次 review 30 分鐘以上</td>
          <td>改平行 background、預期縮到 10 分鐘</td>
      </tr>
      <tr>
          <td>主 context tokens 在 review 階段增長過快</td>
          <td>Reviewer 沒用 output file、改 prompt 明示「報告寫進檔」</td>
      </tr>
      <tr>
          <td>想複用 reviewer prompt 到不同任務</td>
          <td>維度該重新設計、不是固定一套</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Agent team context 隔離是 LLM-era review 工具的設計模式 — 用 instance 隔離換維度深度跟 context 保護。維度設計要對任務客製化、不要固定不變。</p>
]]></content:encoded></item><item><title>Cadence 同質化是模板的隱形維度</title><link>https://tarrragon.github.io/blog/report/cadence-homogenization-in-batch-writing/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cadence-homogenization-in-batch-writing/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>「模板」有兩個維度、寫作規範通常只 enforce 第一維、第二維是隱形維度：&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>規模對照表、tripwire 條件、失敗模式、回退路徑&lt;/td>
 &lt;td>已被 AGENTS.md 原則八 enforce（情境優先於模板）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cadence 模板&lt;/td>
 &lt;td>段首句句型、段末收尾語、表格前導句、過渡詞、列表收尾結構&lt;/td>
 &lt;td>&lt;strong>未被規範涵蓋&lt;/strong> — 51 vendor 同 cadence、各篇單看都合規、連讀才預期化&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實際案例（backend/07 批量 51 vendor）：51 個 vendor 個別頁的「最短判讀路徑」段都收尾在「四件事任一缺失、就是 X 邊界的待補項目」。51/51 同骨、跨 9 個 service group、跨不同 vendor 性質。每一篇單看符合規範、表格有延伸、無 emoji、章節結構齊、案例正確；連讀 5 篇後讀者預期化、cadence 變成「閱讀時自動跳過的雜訊」。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-cadence-維度最容易失守">為什麼 cadence 維度最容易失守&lt;/h2>
&lt;p>三層原因疊加：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>規範語言只涵蓋 &lt;em>內容&lt;/em> 層、不涵蓋 &lt;em>形式&lt;/em> 層&lt;/strong>：AGENTS.md 原則八寫「不為了整齊把不同案例硬套同一模板」、配的例子是「規模對照、tripwire、失敗模式」；批量寫作時 Claude 把「保留情境差異」自動解讀成「敘事內容不同就行」、cadence / framing 不在規範定義的「模板」內、沒被擋。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>批量寫作的便利成本最低&lt;/strong>：寫第一個 vendor 找到一個「都過 lint + 章節完整 + 表格有延伸」的 framing 後、複製這個骨架到下 50 個 vendor 是最省 token 的選擇；每篇都合規、輸出快、且看不到單篇有問題。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>單篇 review 看不到 cadence 違規&lt;/strong>：cadence 同質化是 &lt;em>跨檔&lt;/em> emergence、單檔 review（一次只讀一份）不會 trigger 訊號；只有「連讀多份 + 對齊 first sentence / closing line」才會看出。連 reviewer agent 也容易漏 — backend/07 三個 reviewer 中、只有寫作規範 reviewer 一句 footnote 提到「cadence 過齊」、其他兩個都沒抓到。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="cadence-自檢方法">Cadence 自檢方法&lt;/h2>
&lt;p>寫批量內容（≥ 5 個同類檔案）時、加入 cadence 抽樣 pass。不是讀全文、是抽固定位置的句子做骨架對照：&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>把每篇每段的第一句並列、看句型骨架是否相同（「X 的 first-class concept 是 Y」）&lt;/td>
 &lt;td>≥ 3 種不同骨架、不是全篇都同一個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>段末收尾語&lt;/td>
 &lt;td>把每篇每段的最後一句並列、看是否反覆用同一個 frame（「四件事任一缺失就是 X」）&lt;/td>
 &lt;td>跨同類段落、收尾語句型該有 50% 以上變化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>表格前導句&lt;/td>
 &lt;td>表格前的引導句、看是否反覆用「下表整理 N 個面向」「以下從 X 維度比較」&lt;/td>
 &lt;td>不該所有表格都用同一個前導模板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>列表收尾結構&lt;/td>
 &lt;td>列表後的承接段、看是否反覆用「以上 N 點任一缺失就是 X」&lt;/td>
 &lt;td>列表收尾不該全都是「N 點任一缺失」結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>過渡詞密度&lt;/td>
 &lt;td>跨檔 grep「實際上 / 換句話說 / 換個角度 / 同樣 / 類似 / 進一步」&lt;/td>
 &lt;td>任一過渡詞在 N 篇中出現率 &amp;gt; 60% 是警訊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>抽樣不需要全做、選 &lt;em>最容易反覆使用&lt;/em> 的 2-3 個位置即可；批量越大、抽樣位置越要多。&lt;/p>
&lt;hr>
&lt;h2 id="cadence-多樣性是正向設計不是事後修補">Cadence 多樣性是「正向設計」、不是「事後修補」&lt;/h2>
&lt;p>寫第 1-3 篇時就該意識：cadence 會被複製到下 N 篇。對策不是「寫完後 review 改」、是「寫第一篇時就刻意製造 N 種 framing 變體、之後在這 N 種裡輪替」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>寫作階段&lt;/th>
 &lt;th>Cadence 策略&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>第 1-3 篇（pilot）&lt;/td>
 &lt;td>刻意寫 3 種不同 framing 變體（如「四件事 / 三條紅線 / 兩個 attestation 點」）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 4-10 篇（早期 batch）&lt;/td>
 &lt;td>輪替使用 pilot 階段的 3 種變體、不固定一個&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第 10+ 篇&lt;/td>
 &lt;td>加入第 4-5 個新 framing 變體、避免變體耗盡再變單調&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>批量結束前&lt;/td>
 &lt;td>抽樣 5 個檔做 cadence 對照、發現同質化提前修&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個做法的關鍵是 &lt;em>變體不是事後抽出來的、是設計階段就準備好的&lt;/em>。一旦寫過 5 篇還沒主動製造變體、就會預設複製第一篇 framing 到所有後續檔案。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>「模板」有兩個維度、寫作規範通常只 enforce 第一維、第二維是隱形維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>內容</th>
          <th>規範狀態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>內容欄位模板</td>
          <td>規模對照表、tripwire 條件、失敗模式、回退路徑</td>
          <td>已被 AGENTS.md 原則八 enforce（情境優先於模板）</td>
      </tr>
      <tr>
          <td>Cadence 模板</td>
          <td>段首句句型、段末收尾語、表格前導句、過渡詞、列表收尾結構</td>
          <td><strong>未被規範涵蓋</strong> — 51 vendor 同 cadence、各篇單看都合規、連讀才預期化</td>
      </tr>
  </tbody>
</table>
<p>實際案例（backend/07 批量 51 vendor）：51 個 vendor 個別頁的「最短判讀路徑」段都收尾在「四件事任一缺失、就是 X 邊界的待補項目」。51/51 同骨、跨 9 個 service group、跨不同 vendor 性質。每一篇單看符合規範、表格有延伸、無 emoji、章節結構齊、案例正確；連讀 5 篇後讀者預期化、cadence 變成「閱讀時自動跳過的雜訊」。</p>
<hr>
<h2 id="為什麼-cadence-維度最容易失守">為什麼 cadence 維度最容易失守</h2>
<p>三層原因疊加：</p>
<ol>
<li>
<p><strong>規範語言只涵蓋 <em>內容</em> 層、不涵蓋 <em>形式</em> 層</strong>：AGENTS.md 原則八寫「不為了整齊把不同案例硬套同一模板」、配的例子是「規模對照、tripwire、失敗模式」；批量寫作時 Claude 把「保留情境差異」自動解讀成「敘事內容不同就行」、cadence / framing 不在規範定義的「模板」內、沒被擋。</p>
</li>
<li>
<p><strong>批量寫作的便利成本最低</strong>：寫第一個 vendor 找到一個「都過 lint + 章節完整 + 表格有延伸」的 framing 後、複製這個骨架到下 50 個 vendor 是最省 token 的選擇；每篇都合規、輸出快、且看不到單篇有問題。</p>
</li>
<li>
<p><strong>單篇 review 看不到 cadence 違規</strong>：cadence 同質化是 <em>跨檔</em> emergence、單檔 review（一次只讀一份）不會 trigger 訊號；只有「連讀多份 + 對齊 first sentence / closing line」才會看出。連 reviewer agent 也容易漏 — backend/07 三個 reviewer 中、只有寫作規範 reviewer 一句 footnote 提到「cadence 過齊」、其他兩個都沒抓到。</p>
</li>
</ol>
<hr>
<h2 id="cadence-自檢方法">Cadence 自檢方法</h2>
<p>寫批量內容（≥ 5 個同類檔案）時、加入 cadence 抽樣 pass。不是讀全文、是抽固定位置的句子做骨架對照：</p>
<table>
  <thead>
      <tr>
          <th>抽樣位置</th>
          <th>比對方式</th>
          <th>預期分佈</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段首句</td>
          <td>把每篇每段的第一句並列、看句型骨架是否相同（「X 的 first-class concept 是 Y」）</td>
          <td>≥ 3 種不同骨架、不是全篇都同一個</td>
      </tr>
      <tr>
          <td>段末收尾語</td>
          <td>把每篇每段的最後一句並列、看是否反覆用同一個 frame（「四件事任一缺失就是 X」）</td>
          <td>跨同類段落、收尾語句型該有 50% 以上變化</td>
      </tr>
      <tr>
          <td>表格前導句</td>
          <td>表格前的引導句、看是否反覆用「下表整理 N 個面向」「以下從 X 維度比較」</td>
          <td>不該所有表格都用同一個前導模板</td>
      </tr>
      <tr>
          <td>列表收尾結構</td>
          <td>列表後的承接段、看是否反覆用「以上 N 點任一缺失就是 X」</td>
          <td>列表收尾不該全都是「N 點任一缺失」結構</td>
      </tr>
      <tr>
          <td>過渡詞密度</td>
          <td>跨檔 grep「實際上 / 換句話說 / 換個角度 / 同樣 / 類似 / 進一步」</td>
          <td>任一過渡詞在 N 篇中出現率 &gt; 60% 是警訊</td>
      </tr>
  </tbody>
</table>
<p>抽樣不需要全做、選 <em>最容易反覆使用</em> 的 2-3 個位置即可；批量越大、抽樣位置越要多。</p>
<hr>
<h2 id="cadence-多樣性是正向設計不是事後修補">Cadence 多樣性是「正向設計」、不是「事後修補」</h2>
<p>寫第 1-3 篇時就該意識：cadence 會被複製到下 N 篇。對策不是「寫完後 review 改」、是「寫第一篇時就刻意製造 N 種 framing 變體、之後在這 N 種裡輪替」：</p>
<table>
  <thead>
      <tr>
          <th>寫作階段</th>
          <th>Cadence 策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1-3 篇（pilot）</td>
          <td>刻意寫 3 種不同 framing 變體（如「四件事 / 三條紅線 / 兩個 attestation 點」）</td>
      </tr>
      <tr>
          <td>第 4-10 篇（早期 batch）</td>
          <td>輪替使用 pilot 階段的 3 種變體、不固定一個</td>
      </tr>
      <tr>
          <td>第 10+ 篇</td>
          <td>加入第 4-5 個新 framing 變體、避免變體耗盡再變單調</td>
      </tr>
      <tr>
          <td>批量結束前</td>
          <td>抽樣 5 個檔做 cadence 對照、發現同質化提前修</td>
      </tr>
  </tbody>
</table>
<p>這個做法的關鍵是 <em>變體不是事後抽出來的、是設計階段就準備好的</em>。一旦寫過 5 篇還沒主動製造變體、就會預設複製第一篇 framing 到所有後續檔案。</p>
<h3 id="dogfood-evidence-2026-05-18n4-sub-threshold-驗證">Dogfood evidence (2026-05-18、N=4 sub-threshold 驗證)</h3>
<p>本卡浮現後立即跑了一次小批量 dogfood：4 篇 deep article（Vault dynamic credential / K8s graceful shutdown / Splunk RBA / Cloudflare Page Shield）寫作前主動規劃 4 種不同 entry framing（標準問題情境 / 痛點宣告 / 概念反向定義 / 對照表驅動）、跨檔 cadence audit 結果：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>backend/07 51 vendor（前批、無 variant 規劃）</th>
          <th>deep article 4 篇（本批、pilot variant）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cadence collapse「任一缺失」族重複</td>
          <td>51/51 (100%)</td>
          <td>0/4 (0%)</td>
      </tr>
      <tr>
          <td>章節 1 entry framing 種類</td>
          <td>1 種</td>
          <td>4 種</td>
      </tr>
      <tr>
          <td>過渡詞密度（實際上 / 進一步 等）</td>
          <td>未量化（同質化嚴重）</td>
          <td>全 0 hits</td>
      </tr>
      <tr>
          <td>Lint / emoji / MD036 違規</td>
          <td>0</td>
          <td>0</td>
      </tr>
  </tbody>
</table>
<p>兩個重點驗證：</p>
<ol>
<li><strong>Sub-threshold（N &lt; 5）仍適用</strong>：原本 pilot 表格寫「第 1-3 篇刻意寫 3 種變體」、預設批量 ≥ 5 篇；實測 N=4 sub-threshold 配 4 種 variant 也能完全錯開 cadence</li>
<li><strong>Pilot phase 邊際成本低於 batch 後 polish</strong>：寫作前花 ~5 分鐘規劃 4 種 framing variant、vs backend/07 51 vendor 批量後 polish ~30-60 分鐘改 51 處 cadence — 預先設計成本 &lt; 事後修正成本 ~10 倍</li>
</ol>
<h3 id="update-n5-full-threshold--同-vendor-sub-tool-系列驗證">Update: N=5 full-threshold + 同 vendor sub-tool 系列驗證</h3>
<p>第一次 N=4 驗證後、立即再跑 N=5 full-threshold batch — 5 篇 PostgreSQL sub-tool deep article（Patroni HA / autovacuum tuning / declarative partitioning / logical replication + Debezium / PITR + WAL archiving）。這批比第一批 <em>cadence collapse 風險更高</em> — 同 vendor、同 article type、同 6-section structure、同 audience。</p>
<p>三批 cadence 比較：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>backend/07 51 vendor（無規劃）</th>
          <th>deep article 第一批 N=4（跨 vendor）</th>
          <th>deep article 第二批 N=5（同 vendor）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cadence collapse「任一缺失」族重複</td>
          <td>51/51 (100%)</td>
          <td>0/4 (0%)</td>
          <td>0/5 (0%)</td>
      </tr>
      <tr>
          <td>章節 1 entry framing 種類</td>
          <td>1</td>
          <td>4</td>
          <td>5</td>
      </tr>
      <tr>
          <td>過渡詞密度</td>
          <td>未量化</td>
          <td>全 0 hits</td>
          <td>全 0 hits</td>
      </tr>
      <tr>
          <td>共同變數</td>
          <td>11 章節結構 + 表格深化</td>
          <td>6-section deep article</td>
          <td>6-section + 同 vendor + 同 audience</td>
      </tr>
  </tbody>
</table>
<p>額外驗證（補既有 sub-threshold 驗證）：</p>
<ol start="3">
<li><strong>Full-threshold N=5 variant 不耗盡</strong>：5 種 variant（lifecycle-driven / pain-driven / concept-reversed / table-driven / standard 6-section）都對應主題本質、沒有「為了不同而不同」、5 篇骨架完全錯開</li>
<li><strong>同 vendor 同 article type 仍可錯開</strong>：理論上 <em>同 vendor 同 type</em> 是 cadence collapse 最高風險場景（共同變數最多）；實測 variant 設計仍能覆蓋、collapse 風險不來自共同 context、來自 <em>寫作前是否主動規劃 variant</em></li>
<li><strong>批次間 sample size 邊界更寬</strong>：原 principle 寫 ≥ 5 才適用、實測 <em>N=4 跟 N=5 一樣有效</em>、threshold 5 是 emergence 訊號偵測的閾值、不是 <em>principle 適用</em> 的閾值；變體規劃在 N ≥ 2 就該做</li>
</ol>
<h3 id="update-partial-collapse-實證被動-vs-主動-variant-對照">Update: Partial collapse 實證（被動 vs 主動 variant 對照）</h3>
<p><strong>Partial collapse 定義</strong>：批量內 <em>部分檔案 cadence 收斂、部分錯開</em>、collapse rate 在 0% 跟 100% 之間（典型 30-70%）。跟全 collapse（100%）跟全錯開（0%）的差異在於 <em>混合訊號</em>：同批內存在不同寫作行為（被動 vs 主動 variant 規劃）、cadence 結果反映行為差異。</p>
<p>第三輪 batch 寫 5 篇 migration playbook（跨 vendor、不同 module）、<em>前 3 篇被動寫作、後 2 篇主動規劃 variant</em>。結果：</p>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>Variant 規劃</th>
          <th>章節 1 entry framing</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 Splunk → Elastic</td>
          <td>被動</td>
          <td>「為什麼遷：X / Y / Z 三條 driver」</td>
      </tr>
      <tr>
          <td>2 Redis → DragonflyDB</td>
          <td>被動</td>
          <td>「為什麼遷：X / Y / Z 三條 driver」</td>
      </tr>
      <tr>
          <td>3 Postgres → Aurora</td>
          <td>被動</td>
          <td>「為什麼遷：X / Y / Z 三條 driver」</td>
      </tr>
      <tr>
          <td>4 Datadog → Grafana</td>
          <td>主動</td>
          <td>「$50K/month bill 拆解」</td>
      </tr>
      <tr>
          <td>5 Kafka ↔ NATS</td>
          <td>主動</td>
          <td>「『Kafka → NATS migration』字面上不成立」</td>
      </tr>
  </tbody>
</table>
<p><strong>3/5 collapse、2/5 錯開</strong> = partial collapse。</p>
<p>五批 cadence rate 對照：</p>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>Sample</th>
          <th>Variant 規劃</th>
          <th>Collapse rate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>backend/07 vendor batch</td>
          <td>N=51</td>
          <td>無</td>
          <td>51/51 (100%)</td>
      </tr>
      <tr>
          <td>Deep article 第一批（跨 vendor）</td>
          <td>N=4</td>
          <td>主動</td>
          <td>0/4 (0%)</td>
      </tr>
      <tr>
          <td>Deep article 第二批（同 vendor）</td>
          <td>N=5</td>
          <td>主動</td>
          <td>0/5 (0%)</td>
      </tr>
      <tr>
          <td>Migration playbook 第一輪（混合）</td>
          <td>N=5</td>
          <td><strong>3 被動 + 2 主動</strong></td>
          <td><strong>3/5 (60%)</strong></td>
      </tr>
      <tr>
          <td>Migration playbook 第二輪（漏類 + 標準）</td>
          <td>N=5</td>
          <td><strong>全 5 主動（學第一輪教訓）</strong></td>
          <td><strong>0/5 (0%)</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>主題語意 attractor 定義（atomic）</strong>：批量寫作中、<em>主題本身的語意結構</em> 對 framing 選擇產生的隱形吸引力 — 例如 migration 主題天然引出「為什麼遷：X / Y / Z driver」開頭、SIEM rule 翻譯天然引出「先 audit 再 translate」開頭。這是 <a href="../compliance-optimum-converges-cadence/">#123 多重硬規範收斂 cadence</a> 的 <em>內容驅動子類型</em>：#123 處理的是 <em>外部 constraint</em>（章節結構 + lint 規則）收斂 cadence、本概念處理的是 <em>主題內部語意</em> 收斂 cadence；兩者機制同骨、attractor 來源不同。</p>
<p>三個關鍵 finding：</p>
<ol start="6">
<li><strong>主題語意 attractor 跟主題相似性正相關</strong>：5 篇 migration playbook 都圍繞「為什麼換 vendor」、entry 自然收斂到「driver list」格式；migration 主題的語意 attractor 比結構 constraint 更強</li>
<li><strong>Sample size 不能解 cadence collapse</strong>：N=5 跟前批 N=5 全錯開差異在 <em>variant 規劃</em>、不是 sample size；證實本卡論斷「variant 規劃必須主動、不是 N≥5 自動避免」</li>
<li><strong>Partial collapse 是 natural attractor 訊號、不是 principle 強化證據</strong>：本批 3/5 collapse 提供 <em>主題語意 attractor 強度</em> 的量化訊號（在無 variant 規劃時 60% 同質化）、但不增強既有 principle 的論證力 — principle 在前兩批已穩定、本批只是揭露新 attractor 來源；後續寫作流程應 <em>預期</em> 主題相似批次的 collapse 風險、不是樂觀假設</li>
<li><strong>第二輪 migration playbook 全主動 variant 在 N=5 同主題下 collapse 0/5</strong>：學第一輪教訓、第二輪 5 篇寫前 <em>主動列 5 種 entry framing variant</em>（meta-reflection / paradox / decision matrix / code-led / reverse definition）、跨檔 cadence audit 結果 0/5 collapse；同主題（migration playbook）+ 同 N=5、唯一變數是 <em>variant 規劃完整度</em>；證實 variant 規劃是 <em>cadence 結果的唯一因果變數</em>、不是「主題不同自然錯開」</li>
</ol>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規範只列「內容欄位不可模板化」、沒列 cadence</td>
          <td>Cadence 同質化合規無感、批量產出後才浮現</td>
      </tr>
      <tr>
          <td>批量寫作前不準備 framing 變體</td>
          <td>第一篇 cadence 被複製到 N 篇、修正成本 = N × 重寫</td>
      </tr>
      <tr>
          <td>Review 用單檔 frame</td>
          <td>跨檔同質化抓不到、需要跨檔抽樣對照</td>
      </tr>
      <tr>
          <td>看到 cadence 過齊就改個別檔</td>
          <td>修不到根因 — 沒準備變體、改完一個下次還是會同質化</td>
      </tr>
      <tr>
          <td>Cadence 視為「寫作風格、不算違規」</td>
          <td>對單篇成立、對批量不成立；連讀預期化就是品質損失</td>
      </tr>
      <tr>
          <td>Reviewer prompt 沒明示「比對跨檔 first/closing」</td>
          <td>Reviewer 抓不到 emergence-class 違規</td>
      </tr>
  </tbody>
</table>
<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 在「寫作骨架」維度的具體實例 — 複製第一篇 framing 最便利、但意圖（情境化敘事）失準</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>補一條 frame — multi-pass 該加「跨檔 cadence 抽樣」這輪、單檔 frame 抓不到本卡反模式</td>
      </tr>
      <tr>
          <td><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據</a></td>
          <td>同骨 pattern — 寫作規則執行時、字面合規（正向陳述 / 不模板化）但行為失準（cadence 同質 / 結論空降）</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點</a></td>
          <td>互補軸 — #114 是 frame 顆粒度（規則 vs 字句層）、本卡是 cadence 維度（內容 vs 形式層）</td>
      </tr>
      <tr>
          <td><a href="../cross-case-synthesized-frame-must-be-labeled/">#117 跨多 case 合成的 frame 必須標為章節合成</a></td>
          <td>Sibling — 都是「合規但有隱形偏差」族；#117 是引用層、本卡是骨架層</td>
      </tr>
      <tr>
          <td><a href="../content-structure-by-max-diff-dimension/">#127 Process content 結構由最大差異維度決定</a></td>
          <td>結構 layer 對偶 — 本卡處理「同 type 內 framing collapse」、#127 處理「跨 type 套錯結構」；兩者都跟「主題語意 attractor」相關</td>
      </tr>
      <tr>
          <td><a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></td>
          <td>具體 SOP — 本卡 update 段引用的 5 篇 migration playbook batch 是該 methodology 的 dogfood、partial collapse 案例都在那批</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>連讀同 batch 3-5 篇後、感覺「節奏一樣」</td>
          <td>Cadence 同質化、跑跨檔抽樣對照確認</td>
      </tr>
      <tr>
          <td>段末收尾語在 batch 內出現率 &gt; 60%</td>
          <td>收尾語模板化、改寫部分檔的收尾</td>
      </tr>
      <tr>
          <td>段首句句型在 batch 內反覆出現</td>
          <td>段首模板化、補 framing 變體</td>
      </tr>
      <tr>
          <td>批量 ≥ 5 篇但寫作前沒準備 framing 變體</td>
          <td>預設會同質化、補 pilot 階段 3 種變體</td>
      </tr>
      <tr>
          <td>Reviewer 報告沒提到「cadence」字眼</td>
          <td>Reviewer prompt 沒明示跨檔 frame、要補</td>
      </tr>
      <tr>
          <td>「四件事 / 三點 / 兩個 trade-off」反覆出現</td>
          <td>列表收尾結構模板化、改用敘事段或重組視角</td>
      </tr>
      <tr>
          <td>想拿 batch 內某一篇當下次寫作參考</td>
          <td>警訊 — 該篇 cadence 可能會被複製到下批、應準備變體再起筆</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：模板不只是內容欄位的模板、cadence / 句型骨架 / 收尾語也是。批量寫作前準備 framing 變體、寫作中跨檔抽樣對照、不要等 batch 完成後 reviewer 才發現連讀預期化。</p>
]]></content:encoded></item><item><title>多重硬規範同時生效會把 cadence 推向便利解</title><link>https://tarrragon.github.io/blog/report/compliance-optimum-converges-cadence/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/compliance-optimum-converges-cadence/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>多個 constraint 同時 enforce、批量寫作就會把 cadence 收斂到「最便利合規解」。這不是違規、是 &lt;em>合規最佳解的副作用&lt;/em>。&lt;/p>
&lt;p>機制：&lt;/p>
&lt;ol>
&lt;li>N 個硬 constraint 同時 enforce（章節結構 / 表格深化 / 行數範圍 / lint 規則 / frontmatter 完整）&lt;/li>
&lt;li>寫第一篇時 Claude 找到一個 framing 同時滿足所有 N 個 constraint&lt;/li>
&lt;li>寫第二篇起、複製這個 framing 是 &lt;em>合規 + 省 token + 風險最低&lt;/em> 的選擇&lt;/li>
&lt;li>51 篇後、cadence 已經 collapse 到一個 framing、雖然每篇都合規&lt;/li>
&lt;/ol>
&lt;p>backend/07 案例：「11 章節 + 表格延伸段 + 130-160 行 + 零 emoji + 案例回寫」5 個 constraint 同時 enforce 下、「四件事 → 任一缺失就是 X 邊界的待補項目」是合規最便利 framing。51/51 都用了。&lt;/p>
&lt;hr>
&lt;h2 id="constraint-越多cadence-收斂越快">Constraint 越多、cadence 收斂越快&lt;/h2>
&lt;p>關鍵直覺：constraint 是 &lt;em>過濾器&lt;/em>、constraint 越多、能通過所有過濾器的 framing 種類就越少；批量寫作下、Claude 會選 &lt;em>第一個發現的可行 framing&lt;/em> 並複製。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Constraint 數&lt;/th>
 &lt;th>可通過的 framing 種類&lt;/th>
 &lt;th>批量同質化風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>0-1（自由寫）&lt;/td>
 &lt;td>幾乎無限&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2-3&lt;/td>
 &lt;td>多種&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4-5&lt;/td>
 &lt;td>幾種&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6+&lt;/td>
 &lt;td>1-2 種&lt;/td>
 &lt;td>極高、不可避免&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這跟 over-constraint 設計問題同骨：要求越具體、解空間越小、批量後解就會集中到少數幾個。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼這個-attractor-規範擋不住">為什麼這個 attractor 規範擋不住&lt;/h2>
&lt;p>對應「為什麼 cadence 維度 &lt;a href="../cadence-homogenization-in-batch-writing/">#122&lt;/a> 失守」、本卡是 &lt;em>機制側&lt;/em> 解釋：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>每篇單看都合規&lt;/strong>：constraint 設計成「單檔通過 / 不通過」、沒有「跨檔 framing 變異性」這個 constraint、所以 single-file lint 永遠 pass&lt;/li>
&lt;li>&lt;strong>複製是 Claude 的 cost optimum&lt;/strong>：批量第 N 篇複製第 1 篇骨架 = 最少新 token、最少 risk、最快輸出；除非有反向壓力、預設行為就是複製&lt;/li>
&lt;li>&lt;strong>規範本身鼓勵「找一個都過的 framing」&lt;/strong>：要求章節齊全 + 表格深化、Claude 自然會收斂到「對所有 vendor 都適用」的 framing；越通用的 framing、cadence 越單一&lt;/li>
&lt;/ul>
&lt;p>「對所有 vendor 都適用」跟「對每個 vendor 都到位」是兩件事 — 通用 framing 不會錯、但會 &lt;em>只到位最小公分母&lt;/em>。批量寫作下、最小公分母 framing 大量複製就是 cadence 同質化。&lt;/p>
&lt;hr>
&lt;h2 id="對策拉開-constraint-或加-anti-template-constraint">對策：拉開 constraint 或加 anti-template constraint&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>拉開 constraint&lt;/td>
 &lt;td>允許 framing 多樣（如「11 章節結構必、但章節內部敘事不限定 frame」）&lt;/td>
 &lt;td>失去部分一致性、換來 cadence 多樣性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>加 anti-template constraint&lt;/td>
 &lt;td>在硬規範裡列「同 batch 內 framing 變體至少 3 種」、「段首句句型分佈」&lt;/td>
 &lt;td>規範複雜度上升、執行需要跨檔抽樣機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pilot phase 強制&lt;/td>
 &lt;td>寫前 3 篇時刻意產出 3 種不同 framing、其他篇從這 3 種輪替&lt;/td>
 &lt;td>前期成本上升、批量整體成本平攤後仍便宜&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>切小 batch + frame 變更&lt;/td>
 &lt;td>每 ≤ 10 篇換一次 dominant frame、不要一個 batch 寫 51 篇&lt;/td>
 &lt;td>批次數上升、單批 review 成本下降&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實務 default：&lt;strong>Pilot phase 強制 + 加 anti-template constraint&lt;/strong>。先在 pilot 階段準備變體、再用規範要求跨檔抽樣、雙層防護。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>多個 constraint 同時 enforce、批量寫作就會把 cadence 收斂到「最便利合規解」。這不是違規、是 <em>合規最佳解的副作用</em>。</p>
<p>機制：</p>
<ol>
<li>N 個硬 constraint 同時 enforce（章節結構 / 表格深化 / 行數範圍 / lint 規則 / frontmatter 完整）</li>
<li>寫第一篇時 Claude 找到一個 framing 同時滿足所有 N 個 constraint</li>
<li>寫第二篇起、複製這個 framing 是 <em>合規 + 省 token + 風險最低</em> 的選擇</li>
<li>51 篇後、cadence 已經 collapse 到一個 framing、雖然每篇都合規</li>
</ol>
<p>backend/07 案例：「11 章節 + 表格延伸段 + 130-160 行 + 零 emoji + 案例回寫」5 個 constraint 同時 enforce 下、「四件事 → 任一缺失就是 X 邊界的待補項目」是合規最便利 framing。51/51 都用了。</p>
<hr>
<h2 id="constraint-越多cadence-收斂越快">Constraint 越多、cadence 收斂越快</h2>
<p>關鍵直覺：constraint 是 <em>過濾器</em>、constraint 越多、能通過所有過濾器的 framing 種類就越少；批量寫作下、Claude 會選 <em>第一個發現的可行 framing</em> 並複製。</p>
<table>
  <thead>
      <tr>
          <th>Constraint 數</th>
          <th>可通過的 framing 種類</th>
          <th>批量同質化風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0-1（自由寫）</td>
          <td>幾乎無限</td>
          <td>低</td>
      </tr>
      <tr>
          <td>2-3</td>
          <td>多種</td>
          <td>中</td>
      </tr>
      <tr>
          <td>4-5</td>
          <td>幾種</td>
          <td>高</td>
      </tr>
      <tr>
          <td>6+</td>
          <td>1-2 種</td>
          <td>極高、不可避免</td>
      </tr>
  </tbody>
</table>
<p>這跟 over-constraint 設計問題同骨：要求越具體、解空間越小、批量後解就會集中到少數幾個。</p>
<hr>
<h2 id="為什麼這個-attractor-規範擋不住">為什麼這個 attractor 規範擋不住</h2>
<p>對應「為什麼 cadence 維度 <a href="../cadence-homogenization-in-batch-writing/">#122</a> 失守」、本卡是 <em>機制側</em> 解釋：</p>
<ul>
<li><strong>每篇單看都合規</strong>：constraint 設計成「單檔通過 / 不通過」、沒有「跨檔 framing 變異性」這個 constraint、所以 single-file lint 永遠 pass</li>
<li><strong>複製是 Claude 的 cost optimum</strong>：批量第 N 篇複製第 1 篇骨架 = 最少新 token、最少 risk、最快輸出；除非有反向壓力、預設行為就是複製</li>
<li><strong>規範本身鼓勵「找一個都過的 framing」</strong>：要求章節齊全 + 表格深化、Claude 自然會收斂到「對所有 vendor 都適用」的 framing；越通用的 framing、cadence 越單一</li>
</ul>
<p>「對所有 vendor 都適用」跟「對每個 vendor 都到位」是兩件事 — 通用 framing 不會錯、但會 <em>只到位最小公分母</em>。批量寫作下、最小公分母 framing 大量複製就是 cadence 同質化。</p>
<hr>
<h2 id="對策拉開-constraint-或加-anti-template-constraint">對策：拉開 constraint 或加 anti-template constraint</h2>
<p>兩條互補路徑：</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>做法</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>拉開 constraint</td>
          <td>允許 framing 多樣（如「11 章節結構必、但章節內部敘事不限定 frame」）</td>
          <td>失去部分一致性、換來 cadence 多樣性</td>
      </tr>
      <tr>
          <td>加 anti-template constraint</td>
          <td>在硬規範裡列「同 batch 內 framing 變體至少 3 種」、「段首句句型分佈」</td>
          <td>規範複雜度上升、執行需要跨檔抽樣機制</td>
      </tr>
      <tr>
          <td>Pilot phase 強制</td>
          <td>寫前 3 篇時刻意產出 3 種不同 framing、其他篇從這 3 種輪替</td>
          <td>前期成本上升、批量整體成本平攤後仍便宜</td>
      </tr>
      <tr>
          <td>切小 batch + frame 變更</td>
          <td>每 ≤ 10 篇換一次 dominant frame、不要一個 batch 寫 51 篇</td>
          <td>批次數上升、單批 review 成本下降</td>
      </tr>
  </tbody>
</table>
<p>實務 default：<strong>Pilot phase 強制 + 加 anti-template constraint</strong>。先在 pilot 階段準備變體、再用規範要求跨檔抽樣、雙層防護。</p>
<p>Dogfood 驗證見 <a href="../cadence-homogenization-in-batch-writing/#dogfood-evidence-2026-05-18-n4-sub-threshold-%e9%a9%97%e8%ad%89">#122 cadence 同質化</a> — 4 篇 deep article batch 用 <em>pilot phase 4 種 variant</em> 取代「事後 polish」、cadence collapse 從前批 100% 降到 0%、修正成本省 ~10 倍。本卡的「拉開 constraint」對策獲實證。</p>
<hr>
<h2 id="不是只發生在寫作">不是只發生在「寫作」</h2>
<p>同骨機制在其他批量產出任務上也成立：</p>
<ul>
<li><strong>Code generation</strong>：用同一 LLM 一次生 N 個 service 的 boilerplate、結構會收斂到同一 framing（同樣的 error handling pattern、同樣的 log 格式）</li>
<li><strong>Test case 批量寫</strong>：N 個 unit test 都用同一個 setup-act-assert framing、覆蓋面看似齊但其實只測一種 axis</li>
<li><strong>API doc 批量寫</strong>：N 個 endpoint doc 都用同一段「方法 / 參數 / 回傳」三段式、抓不到 endpoint-specific 邊界</li>
</ul>
<p>這些都是 constraint 設計的 collapse — 只是發生在不同 surface。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規範堆疊不評估 attractor 副作用</td>
          <td>Constraint 越多 cadence 越單一、規範自身成為同質化 root cause</td>
      </tr>
      <tr>
          <td>認為「合規 = 品質」</td>
          <td>51 篇都合規但連讀預期化、合規是必要不充分</td>
      </tr>
      <tr>
          <td>批量寫作不切 batch、一次寫 50+ 檔</td>
          <td>Cadence collapse 風險最大、修正成本 N 倍</td>
      </tr>
      <tr>
          <td>發現同質化後加更多 constraint</td>
          <td>Over-constraint、解空間更窄、cadence 反而更收斂</td>
      </tr>
      <tr>
          <td>Pilot phase 跳過、直接寫批量</td>
          <td>沒準備變體、第一篇 framing 自動成 dominant</td>
      </tr>
      <tr>
          <td>把 cadence 問題歸因「Claude 偷懶」、不是 constraint 設計問題</td>
          <td>換 model 還是會發生、根因在 constraint 設計、不是執行者</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>Sibling — #122 是 <em>症狀</em> 卡（cadence 同質化是模板）、本卡是 <em>機制</em> 卡（為什麼會發生）；兩張一起讀</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>本卡是 #67 在「批量產出」的具體機制；複製合規 framing 最便利、跨檔意圖對齊失準</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 Single Source of Truth</a></td>
          <td>互補 — SSoT 處理「值的住址只能一處」、本卡處理「framing 的住址不能只有一處」；兩者是 SSoT vs anti-SSoT 的不同 surface</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>本卡是 #82 在 constraint 設計的具體 case — 字面合規（章節 / 表格 / 行數）+ 行為失準（cadence 同質）</td>
      </tr>
      <tr>
          <td><a href="../capability-gap-three-layer-escalation/">#86 Capability gap 三層對策階梯</a></td>
          <td>互補 — 同質化問題不該只用 L1（提醒 Claude 變化）、要 L2（pilot phase）或 L3（規範擴寫 anti-template）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規範條目 ≥ 5 條且 enforce 同一檔</td>
          <td>評估 attractor 風險、是否該拉開或加 anti-template constraint</td>
      </tr>
      <tr>
          <td>一個 batch 計畫寫 ≥ 10 個同類檔</td>
          <td>切小 batch、或加 pilot phase 強制變體</td>
      </tr>
      <tr>
          <td>Pilot phase 只寫 1-2 個就進批量</td>
          <td>沒準備 framing 變體、預設會 collapse</td>
      </tr>
      <tr>
          <td>想再加新 constraint 解決品質問題</td>
          <td>警訊 — 加多會更 collapse、考慮拉開或換層</td>
      </tr>
      <tr>
          <td>Review 報告說「都合規」</td>
          <td>不夠、加跨檔 cadence 抽樣 frame</td>
      </tr>
      <tr>
          <td>批量寫完 reviewer 才發現同質化</td>
          <td>Review 時機太晚、改 stage 內抽樣</td>
      </tr>
      <tr>
          <td>想複用上批 framing 寫下批</td>
          <td>警訊 — 復用 dominant framing 會把同質化跨 batch 擴散</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：多重硬規範同時生效時、cadence 收斂到合規最便利解是預設行為、不是違規。對策不是加更多 constraint、是拉開 constraint 或強制 pilot phase 準備變體；規範設計時要評估 attractor 副作用、不是只看「單檔有沒有過」。</p>
]]></content:encoded></item><item><title>Emergence-class 違規規則化不了、要 stage 0 變體規劃 + stage 內抽樣兩層</title><link>https://tarrragon.github.io/blog/report/emergence-violations-need-in-stream-sampling/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/emergence-violations-need-in-stream-sampling/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>違規類型決定 enforcement &lt;em>時機 + 機制&lt;/em>。把所有違規都丟給 batch 完成後的 reviewer 是錯的；同樣錯的是 &lt;em>只靠生成中抽樣&lt;/em>、沒有 stage 0 的主動變體規劃。Emergence 違規需要 &lt;em>兩層防護&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Stage 0 主動設計層&lt;/strong>：寫批量前列 N 種 framing variant、分配給對應主題；這層決定 &lt;em>cadence 是否錯開的根因&lt;/em>&lt;/li>
&lt;li>&lt;strong>Stage 內被動監測層&lt;/strong>：生成中抽樣 audit、發現 collapse 立即修方向；這層 &lt;em>偵測&lt;/em> 而不是 &lt;em>設計&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>兩層缺一不可：跳過 stage 0、被動抽樣不會自動發現 &lt;em>主題語意 attractor&lt;/em>（相似主題天然引出的 framing collapse、見 &lt;a href="../cadence-homogenization-in-batch-writing/">#122 cadence 同質化&lt;/a> Update 段定義；migration playbook 3/5 collapse 即此實證、見本卡「Update: 被動寫作下&amp;hellip;」段）；跳過 stage 內抽樣、stage 0 設計可能在中途 drift 沒被 catch。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>違規類型&lt;/th>
 &lt;th>識別形式&lt;/th>
 &lt;th>Enforcement 時機&lt;/th>
 &lt;th>工具&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>字面違規&lt;/td>
 &lt;td>單檔可 regex 偵測（emoji、裸 URL、粗體當標題）&lt;/td>
 &lt;td>Pre-commit / pre-push&lt;/td>
 &lt;td>mdtools / regex hook&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結構違規&lt;/td>
 &lt;td>單檔可機制偵測（章節缺失、frontmatter 必填、broken link）&lt;/td>
 &lt;td>Linter / build&lt;/td>
 &lt;td>mdtools lint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Emergence 違規&lt;/td>
 &lt;td>跨檔比對才偵測（cadence 同質化、語氣漂移、frame 重複）&lt;/td>
 &lt;td>&lt;strong>Stage 0 設計 + Stage 內監測 兩層&lt;/strong>&lt;/td>
 &lt;td>Stage 0 variant 規劃 + 寫作流程內 checkpoint、不是 hook&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>backend/07 案例對照：51 個 vendor 字面違規 0、結構違規 0、emergence 違規（cadence 同質化）51/51；後者三個 reviewer 中只有一個 footnote 提到、是因為 reviewer 一次審 51 檔、emergence 訊號夠強才看出 — &lt;em>如果只審 5 檔、emergence 訊號還不夠強、會被漏掉&lt;/em>。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-emergence-class-違規規則化不了">為什麼 emergence-class 違規規則化不了&lt;/h2>
&lt;p>字面違規可以寫成 regex（&lt;code>rg &amp;quot;✅|❌&amp;quot;&lt;/code>）、結構違規可以寫成 grammar 規則（章節必須有 N 個 H2）。Emergence 違規的特徵：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>跨檔才能偵測&lt;/strong>：單檔 cadence 沒問題、N 檔 cadence 對齊才是違規&lt;/li>
&lt;li>&lt;strong>規則化會 over-fit&lt;/strong>：寫「段末不可用『四件事任一缺失』」會把這句正常用法也擋掉；寫「段首句句型分佈要 ≥ 3 種」需要先語法剖析、複雜度爆炸&lt;/li>
&lt;li>&lt;strong>訊號隨樣本數變化&lt;/strong>：5 檔比對訊號弱、50 檔比對訊號強；linter 沒有「批次」概念、只看單檔&lt;/li>
&lt;li>&lt;strong>跟風格邊界模糊&lt;/strong>：cadence 一致 vs cadence 同質、之間是漸層、threshold 因領域而異&lt;/li>
&lt;/ol>
&lt;p>結論：emergence 違規不能靠 hook / linter / 字面規則攔、只能靠 &lt;em>流程設計&lt;/em> 在生成中 trigger 抽樣 review。&lt;/p>
&lt;hr>
&lt;h2 id="stage-內抽樣的設計">Stage 內抽樣的設計&lt;/h2>
&lt;p>對 &lt;a href="https://tarrragon.github.io/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &amp;#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &amp;#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">case-first-module-workflow&lt;/a> 補強：stage 2（內容生成）內部加入 cadence checkpoint、不要等 stage 3 reviewer 才發現。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>違規類型決定 enforcement <em>時機 + 機制</em>。把所有違規都丟給 batch 完成後的 reviewer 是錯的；同樣錯的是 <em>只靠生成中抽樣</em>、沒有 stage 0 的主動變體規劃。Emergence 違規需要 <em>兩層防護</em>：</p>
<ul>
<li><strong>Stage 0 主動設計層</strong>：寫批量前列 N 種 framing variant、分配給對應主題；這層決定 <em>cadence 是否錯開的根因</em></li>
<li><strong>Stage 內被動監測層</strong>：生成中抽樣 audit、發現 collapse 立即修方向；這層 <em>偵測</em> 而不是 <em>設計</em></li>
</ul>
<p>兩層缺一不可：跳過 stage 0、被動抽樣不會自動發現 <em>主題語意 attractor</em>（相似主題天然引出的 framing collapse、見 <a href="../cadence-homogenization-in-batch-writing/">#122 cadence 同質化</a> Update 段定義；migration playbook 3/5 collapse 即此實證、見本卡「Update: 被動寫作下&hellip;」段）；跳過 stage 內抽樣、stage 0 設計可能在中途 drift 沒被 catch。</p>
<table>
  <thead>
      <tr>
          <th>違規類型</th>
          <th>識別形式</th>
          <th>Enforcement 時機</th>
          <th>工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>字面違規</td>
          <td>單檔可 regex 偵測（emoji、裸 URL、粗體當標題）</td>
          <td>Pre-commit / pre-push</td>
          <td>mdtools / regex hook</td>
      </tr>
      <tr>
          <td>結構違規</td>
          <td>單檔可機制偵測（章節缺失、frontmatter 必填、broken link）</td>
          <td>Linter / build</td>
          <td>mdtools lint</td>
      </tr>
      <tr>
          <td>Emergence 違規</td>
          <td>跨檔比對才偵測（cadence 同質化、語氣漂移、frame 重複）</td>
          <td><strong>Stage 0 設計 + Stage 內監測 兩層</strong></td>
          <td>Stage 0 variant 規劃 + 寫作流程內 checkpoint、不是 hook</td>
      </tr>
  </tbody>
</table>
<p>backend/07 案例對照：51 個 vendor 字面違規 0、結構違規 0、emergence 違規（cadence 同質化）51/51；後者三個 reviewer 中只有一個 footnote 提到、是因為 reviewer 一次審 51 檔、emergence 訊號夠強才看出 — <em>如果只審 5 檔、emergence 訊號還不夠強、會被漏掉</em>。</p>
<hr>
<h2 id="為什麼-emergence-class-違規規則化不了">為什麼 emergence-class 違規規則化不了</h2>
<p>字面違規可以寫成 regex（<code>rg &quot;✅|❌&quot;</code>）、結構違規可以寫成 grammar 規則（章節必須有 N 個 H2）。Emergence 違規的特徵：</p>
<ol>
<li><strong>跨檔才能偵測</strong>：單檔 cadence 沒問題、N 檔 cadence 對齊才是違規</li>
<li><strong>規則化會 over-fit</strong>：寫「段末不可用『四件事任一缺失』」會把這句正常用法也擋掉；寫「段首句句型分佈要 ≥ 3 種」需要先語法剖析、複雜度爆炸</li>
<li><strong>訊號隨樣本數變化</strong>：5 檔比對訊號弱、50 檔比對訊號強；linter 沒有「批次」概念、只看單檔</li>
<li><strong>跟風格邊界模糊</strong>：cadence 一致 vs cadence 同質、之間是漸層、threshold 因領域而異</li>
</ol>
<p>結論：emergence 違規不能靠 hook / linter / 字面規則攔、只能靠 <em>流程設計</em> 在生成中 trigger 抽樣 review。</p>
<hr>
<h2 id="stage-內抽樣的設計">Stage 內抽樣的設計</h2>
<p>對 <a href="/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/" data-link-title="Case-First &#43; Agent Team Review：教學內容的生產流程" data-link-desc="Case-first &#43; agent team review 的教學內容生產流程：讀案例庫抽 findings、專責 reviewer 平行審查、polish pass 收系統性殘留。防止通用 best practice 被誤包裝成案例揭露。">case-first-module-workflow</a> 補強：stage 2（內容生成）內部加入 cadence checkpoint、不要等 stage 3 reviewer 才發現。</p>
<table>
  <thead>
      <tr>
          <th>寫作進度</th>
          <th>Checkpoint 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1-3 篇</td>
          <td>刻意產出 3 種不同 framing 變體（pilot phase）、人類 / Claude 自審「這 3 篇 cadence 是否真不同」</td>
      </tr>
      <tr>
          <td>第 5 篇</td>
          <td>抽 5 個段首句並列、確認 framing 變體仍在輪替、沒有 collapse 到 dominant</td>
      </tr>
      <tr>
          <td>第 10 篇</td>
          <td>抽 10 個段末收尾語並列、確認收尾語句型分佈 ≥ 3 種</td>
      </tr>
      <tr>
          <td>每 + 10 篇</td>
          <td>重複上述抽樣、發現 collapse 立即回頭加變體、不要繼續寫</td>
      </tr>
      <tr>
          <td>Batch 結束前</td>
          <td>全 batch 跨檔 cadence audit、確認 framing 分佈</td>
      </tr>
  </tbody>
</table>
<p>關鍵：抽樣不是「Reviewer 在 batch 完成後跑」、是「寫作者在生成中跑」。寫第 5 篇之前先回頭看前 5 篇、發現問題就在第 5 篇修方向、不是寫完 50 篇才回頭改 50 個。</p>
<h3 id="dogfood-evidence-2026-05-18n4-sub-threshold-驗證">Dogfood evidence (2026-05-18、N=4 sub-threshold 驗證)</h3>
<p>本卡浮現後立即跑 4 篇 deep article 小批量 dogfood、用 <em>寫作中抽樣 + pilot phase variant</em> 取代 batch 後 reviewer：</p>
<table>
  <thead>
      <tr>
          <th>Checkpoint 位置</th>
          <th>動作</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 篇寫完</td>
          <td>確認自然 framing（標準問題情境）</td>
          <td>OK、為第 2 篇 variant 比對 baseline</td>
      </tr>
      <tr>
          <td>第 2 篇寫前</td>
          <td>主動換 variant（痛點宣告 case-led）</td>
          <td>段首句骨架明顯異於第 1 篇</td>
      </tr>
      <tr>
          <td>第 3 篇寫前</td>
          <td>第三種 variant（概念反向定義）</td>
          <td>三種骨架完全錯開</td>
      </tr>
      <tr>
          <td>第 4 篇寫前</td>
          <td>第四種 variant（對照表驅動）+ 抽前 3 篇章節 1 entry sample audit</td>
          <td>四種骨架完全錯開、過渡詞密度 0、cadence 「任一缺失」族 0 hits</td>
      </tr>
  </tbody>
</table>
<p>對照前批 backend/07 51 vendor（無寫作中 checkpoint）：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>backend/07 51 vendor（batch 後才 review）</th>
          <th>deep article 4 篇（生成中抽樣）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修正成本</td>
          <td>~30-60 分鐘 polish 51 處</td>
          <td>~5 分鐘 / 篇前規劃 + 0 polish</td>
      </tr>
      <tr>
          <td>Cadence collapse 比例</td>
          <td>51/51 (100%)</td>
          <td>0/4 (0%)</td>
      </tr>
      <tr>
          <td>發現 collapse 時的 sample 數</td>
          <td>51（已寫完才發現）</td>
          <td>1-3（生成中即時調方向）</td>
      </tr>
  </tbody>
</table>
<p>兩個驗證：</p>
<ol>
<li><strong>Stage 內抽樣在 sub-threshold N=4 仍有效</strong>：原本 checkpoint 表格寫第 5 / 10 篇抽樣、預設批量 ≥ 5；實測 <em>寫每篇前都做一次 entry framing variant check</em> 在 N=4 也能完全錯開 cadence</li>
<li><strong>生成中抽樣的邊際成本 &laquo; batch 後 polish 成本</strong>：每篇前 ~1-2 分鐘 cadence check vs batch 後修 51 處 ~30-60 分鐘 — 比例 ~10-15 倍。本卡論斷「修正成本 N 倍」獲實證</li>
</ol>
<h3 id="update-n5-full-threshold-checkpoint-排程驗證">Update: N=5 full-threshold checkpoint 排程驗證</h3>
<p>第一次 N=4 後立即跑 N=5 full-threshold batch（5 篇 PostgreSQL sub-tool）、驗證 checkpoint 排程在 ≥ 5 真實閾值的表現：</p>
<table>
  <thead>
      <tr>
          <th>Checkpoint 位置</th>
          <th>N=4 batch 動作</th>
          <th>N=5 batch 動作</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 篇寫完（20%）</td>
          <td>確認 baseline framing</td>
          <td>確認 baseline framing（lifecycle）</td>
          <td>OK、N=5 抽樣訊號比 N=4 略強</td>
      </tr>
      <tr>
          <td>第 2 篇寫前（20%）</td>
          <td>主動換 variant</td>
          <td>主動換 variant（pain-driven）</td>
          <td>兩種 framing 對照成立</td>
      </tr>
      <tr>
          <td>第 3 篇寫前（40-60%）</td>
          <td>第三種 variant</td>
          <td>第三種 variant（concept-reversed）</td>
          <td>三種對照、cadence drift 機率變大</td>
      </tr>
      <tr>
          <td>第 4 篇寫前（60-80%）</td>
          <td>第四種 variant + 抽前 3 篇 audit</td>
          <td>第四種 variant（table-driven）+ 抽前 3 篇 entry sample audit</td>
          <td>四種對照、確認 framing 不耗盡</td>
      </tr>
      <tr>
          <td>第 5 篇寫前（80%）</td>
          <td>-</td>
          <td>第五種 variant（standard 6-section）+ 抽前 4 篇 audit</td>
          <td>五種對照、進度 80% audit 信號最強</td>
      </tr>
      <tr>
          <td>批次完成（100%）</td>
          <td>全 batch 跨檔 cadence audit</td>
          <td>全 batch 跨檔 cadence audit</td>
          <td>N=5 audit 樣本大、訊號更強</td>
      </tr>
  </tbody>
</table>
<p>兩批對照：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>N=4 batch（跨 vendor）</th>
          <th>N=5 batch（同 vendor sub-tool 系列）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修正成本 / 篇前規劃</td>
          <td>~5 分鐘 / 篇</td>
          <td>~5 分鐘 / 篇（不變）</td>
      </tr>
      <tr>
          <td>Cadence collapse 比例</td>
          <td>0/4 (0%)</td>
          <td>0/5 (0%)</td>
      </tr>
      <tr>
          <td>進度 20% (1 篇後) 抽樣可發現性</td>
          <td>訊號弱（1 樣本）</td>
          <td>訊號弱（仍 1 樣本）</td>
      </tr>
      <tr>
          <td>進度 80% (4 篇後) 抽樣可發現性</td>
          <td>訊號強（4 對照）</td>
          <td>訊號更強（4 對照 + 進入第 5 篇）</td>
      </tr>
      <tr>
          <td>同 vendor 共同 context 影響</td>
          <td>較低（4 篇跨 vendor）</td>
          <td>高（5 篇同 vendor、collapse 風險最高）</td>
      </tr>
  </tbody>
</table>
<p>額外驗證：</p>
<ol start="3">
<li><strong>進度 10-20% 抽樣訊號偏弱、80% 抽樣最強</strong>：N=5 batch 確認 <em>進度 80% audit</em> 是 emergence 訊號最強位置；原 principle 寫「進度 10-20% 抽樣」是過早、實際 <em>寫前 variant 規劃 + 進度 60-80% audit</em> 組合更穩</li>
<li><strong>同 vendor 同 type 是 collapse 最高風險、checkpoint 仍 cover</strong>：N=5 batch 共同 context 比 N=4 多（同 vendor / 同 audience / 同 article type）、本卡論斷 emergence 風險 = 共同 context × N 成立；checkpoint 設計能 cover 是因為 <em>variant 規劃在 stage 0</em>、不靠 sample size 補</li>
</ol>
<h3 id="update-被動寫作下-stage-internal-checkpoint-仍失效">Update: 被動寫作下 stage-internal checkpoint 仍失效</h3>
<p>第三輪 5 篇 migration playbook 中 <em>前 3 篇被動寫作</em>（沒主動規劃 variant）— stage-internal checkpoint 雖然按時 fired、但因為 <em>沒 variant 預先準備</em>、checkpoint 看到的「不同主題」誤以為 framing 會自然錯開、實際 collapse 到「為什麼遷：X/Y/Z driver」格式：</p>
<table>
  <thead>
      <tr>
          <th>進度</th>
          <th>Checkpoint 觸發</th>
          <th>看到的訊號</th>
          <th>行動</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 篇</td>
          <td>baseline 確認</td>
          <td>「為什麼遷：cost / multi-vendor / cloud-native」</td>
          <td>沒設變體規劃</td>
          <td>第 2 篇預設複製 framing</td>
      </tr>
      <tr>
          <td>第 2 篇</td>
          <td>應該抽樣 audit</td>
          <td>跟第 1 篇都「為什麼遷 X/Y/Z」</td>
          <td><strong>被動接受、認為主題不同就 OK</strong></td>
          <td>第 3 篇也複製</td>
      </tr>
      <tr>
          <td>第 3 篇</td>
          <td>應該抽樣 audit</td>
          <td>連續 3 篇相同 framing</td>
          <td><strong>發現問題、決定後 2 篇換 variant</strong></td>
          <td>後 2 篇主動 variant、cadence 部分挽救</td>
      </tr>
      <tr>
          <td>第 4 篇</td>
          <td>active variant</td>
          <td>cost-driven entry、跟前 3 篇骨架不同</td>
          <td>持續 variant</td>
          <td>OK</td>
      </tr>
      <tr>
          <td>第 5 篇</td>
          <td>active variant</td>
          <td>paradigm contrast entry</td>
          <td>全 batch audit</td>
          <td>3/5 collapse、2/5 不同</td>
      </tr>
  </tbody>
</table>
<p>兩個關鍵 finding：</p>
<ol start="5">
<li><strong>Checkpoint 不夠、變體規劃才是 root</strong>：stage-internal checkpoint 確實 fire、但 <em>沒準備 variant</em> 時 checkpoint 變被動驗證、不是主動防護；本卡原論斷「checkpoint 取代 batch 後 reviewer」需修正為「checkpoint + 預先 variant 規劃 兩層」</li>
<li><strong>主題語意 attractor 是新失效源</strong>：N=5 batch 中前 3 篇都圍繞「為什麼換 vendor」、entry 自然 collapse 到 driver list；這個 attractor 比結構 constraint 更強、未來寫作要 <em>預先列 framing 變體</em> 而不是 <em>依賴 checkpoint 提醒換</em></li>
</ol>
<p>修正後的 Stage 內 checkpoint 排程（補 stage 0 變體規劃）：</p>
<table>
  <thead>
      <tr>
          <th>寫作進度</th>
          <th>Checkpoint 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Stage 0（寫前）</strong></td>
          <td><strong>列 N 種 framing 變體 + 對應 N 篇主題分配</strong>（新增、原本缺失）</td>
      </tr>
      <tr>
          <td>第 1-3 篇（pilot phase）</td>
          <td>按 stage 0 分配執行、人類 / Claude 自審「實際 entry framing 跟 stage 0 規劃對齊嗎」</td>
      </tr>
      <tr>
          <td>第 5 篇</td>
          <td>抽 5 個段首句並列、確認 framing 變體仍在輪替</td>
      </tr>
      <tr>
          <td>第 10 篇</td>
          <td>抽 10 個段末收尾語並列、確認句型分佈 ≥ 3 種</td>
      </tr>
      <tr>
          <td>每 + 10 篇</td>
          <td>重複抽樣、發現 collapse 立即回頭加變體</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<em>Stage 0 變體規劃是必要 step</em>、不能跳；checkpoint 是 <em>監測</em> 工具、不是 <em>設計</em> 工具。</p>
<p>詳細 SOP 跟 5 種 type 的具體應用見 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> — 該 methodology 從 5 篇 migration playbook batch 抽出 stage 0 variant 規劃流程、本卡的「checkpoint 不夠」訊號是該流程的觸發實證。</p>
<h3 id="update2026-05-19第二輪-migration-batch-全主動-variant-驗證">Update（2026-05-19）：第二輪 migration batch 全主動 variant 驗證</h3>
<p>第二輪 migration batch（5 篇）寫前主動列 5 種 entry framing variant、cadence audit 結果 0/5 collapse；跟第一輪 3/5 collapse 對照、唯一差異是 <em>variant 規劃完整度</em>：</p>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>Sample</th>
          <th>Variant 規劃</th>
          <th>Stage-internal checkpoint 結果</th>
          <th>Collapse rate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第一輪（混合）</td>
          <td>N=5</td>
          <td>前 3 篇被動、後 2 篇主動</td>
          <td>被動段 checkpoint 失效</td>
          <td>3/5 (60%)</td>
      </tr>
      <tr>
          <td>第二輪（全主動）</td>
          <td>N=5</td>
          <td>寫前列 5 種 variant、執行對應</td>
          <td>Checkpoint 監測通過</td>
          <td>0/5 (0%)</td>
      </tr>
  </tbody>
</table>
<p>第二輪確認本卡核心論斷：</p>
<ol start="5">
<li><strong>Checkpoint + Stage 0 兩層在全主動下成功</strong>：第二輪 5 篇 stage 0 全列 variant、checkpoint 監測無 collapse alarm、最終 audit 0/5；證實兩層防護在 <em>都執行</em> 下達成 principle 目標</li>
<li><strong>Stage 0 規劃的標準動作</strong>：第二輪 stage 0 動作為「列 5 種 distinct entry framing 候選、對應 5 篇主題分配」— 不是「想到才換」、是 <em>寫第一篇前就完成</em> 的設計步驟</li>
<li><strong>主題相似性不會自動解決 cadence</strong>：第二輪 5 篇都是 migration playbook、主題相似性跟第一輪一樣高；唯一差異是 stage 0 是否做、結果差 60% collapse vs 0% — 確認本卡論斷「checkpoint 不夠變體規劃才是 root」在 <em>主題相似性高</em> 場景下仍成立</li>
</ol>
<hr>
<h2 id="batch-完成後-reviewer-為什麼太晚">Batch 完成後 reviewer 為什麼太晚</h2>
<p>三個成本問題：</p>
<ol>
<li><strong>修正成本 N 倍</strong>：51 篇都同質化、修正要動 51 篇；如果第 5 篇就 catch、只動 5 篇</li>
<li><strong>Cadence 已內化成「正確答案」</strong>：寫完 50 篇後 Claude 已經把 dominant framing 視為「合規最佳解」、要打破比第 5 篇難</li>
<li><strong>Reviewer 訊號要求高樣本</strong>：5 檔不夠 emergence、50 檔才強訊號；但 50 檔出來時修正成本已經爆</li>
</ol>
<p>最佳時機：<em>Sample size 剛夠看出 emergence、且修正成本還可控</em> — 通常是 batch 內 10-20% 進度的位置（51 批量 → 第 5-10 篇）。</p>
<hr>
<h2 id="跟字面--結構違規的時機對照">跟字面 / 結構違規的時機對照</h2>
<p>字面違規（emoji）的 enforcement 鏈：</p>
<ul>
<li>寫作中：Claude 預設不寫 emoji（pre-trained behavior + system prompt）</li>
<li>Pre-commit：mdtools / regex hook 攔</li>
<li>CI：full lint sweep</li>
</ul>
<p>三層防護、字面違規幾乎不可能漏網。</p>
<p>結構違規（章節缺失）的 enforcement 鏈：</p>
<ul>
<li>寫作中：模板 / skeleton 提示</li>
<li>Pre-commit：lint 章節結構</li>
<li>Review：人類 / agent 看章節列表</li>
</ul>
<p>兩層機制、結構違規會被攔。</p>
<p>Emergence 違規目前的 enforcement：</p>
<ul>
<li>寫作中：<strong>無</strong>（cadence 沒提示）</li>
<li>Pre-commit：<strong>無</strong>（regex 攔不到）</li>
<li>Review：Stage 3 reviewer 可能漏（單 reviewer 視野有限）</li>
</ul>
<p>只有 stage 3 reviewer 這一層、且不可靠。本卡的修法是在「寫作中」這層加 stage 內抽樣。</p>
<hr>
<h2 id="不只是寫作emergence-違規的其他例子">不只是寫作：emergence 違規的其他例子</h2>
<table>
  <thead>
      <tr>
          <th>任務類型</th>
          <th>字面違規例</th>
          <th>結構違規例</th>
          <th>Emergence 違規例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫作</td>
          <td>emoji / 裸 URL</td>
          <td>章節缺失 / frontmatter</td>
          <td>Cadence 同質化、語氣漂移、frame 重複</td>
      </tr>
      <tr>
          <td>Code review</td>
          <td>console.log / typo</td>
          <td>缺型別 / 缺 test</td>
          <td>抽象層級不一致、命名漂移、相似函式散落</td>
      </tr>
      <tr>
          <td>Schema 設計</td>
          <td>缺 NOT NULL / 缺 index</td>
          <td>缺 FK / 缺 unique</td>
          <td>命名慣例分裂、欄位順序不一致、表間關係風格不齊</td>
      </tr>
      <tr>
          <td>API doc</td>
          <td>拼字 / broken link</td>
          <td>缺參數說明 / 缺範例</td>
          <td>例子風格不一、術語使用漂移、章節長短差異懸殊</td>
      </tr>
  </tbody>
</table>
<p>三類違規對應三層 enforcement、不能混用工具。</p>
<hr>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把 emergence 違規丟給 hook 解決</td>
          <td>Hook 抓不到、false confidence</td>
      </tr>
      <tr>
          <td>把 emergence 違規丟給 batch 完成後 reviewer</td>
          <td>修正成本 N 倍、cadence 已內化</td>
      </tr>
      <tr>
          <td>寫 batch 不在中段抽樣</td>
          <td>Emergence collapse 後才發現、無法即時修方向</td>
      </tr>
      <tr>
          <td>Reviewer prompt 不明示跨檔比對</td>
          <td>Reviewer 用單檔 frame 審 N 檔、emergence 漏抓</td>
      </tr>
      <tr>
          <td>把 cadence 抽樣只列在「Batch 結束前」</td>
          <td>太晚、跟「Reviewer batch 後跑」沒差</td>
      </tr>
      <tr>
          <td>規範裡寫「不可 cadence 同質化」但不提抽樣機制</td>
          <td>規範文字成立、執行落空</td>
      </tr>
  </tbody>
</table>
<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>本卡是 #82 的時機軸延伸 — #82 分字面 / 行為兩類、本卡分字面 / 結構 / emergence 三類並對應 enforcement 時機</td>
      </tr>
      <tr>
          <td><a href="../cadence-homogenization-in-batch-writing/">#122 Cadence 同質化是模板的隱形維度</a></td>
          <td>配對 — #122 定義違規類型、本卡解 enforcement 時機；兩張一起解 cadence 問題</td>
      </tr>
      <tr>
          <td><a href="../compliance-optimum-converges-cadence/">#123 多重硬規範同時生效會把 cadence 推向便利解</a></td>
          <td>配對 — #123 解釋成因（constraint 收斂）、本卡解 enforcement（時機 + 抽樣）</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸：四個 checkpoint</a></td>
          <td>同骨 pattern — #68 把驗收切「寫之前 / 開發中 / ship 前 / ship 後」、本卡把寫作 review 切時機；都是 enforcement 時機軸</td>
      </tr>
      <tr>
          <td><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review 的 scope 要蓋同類風險區</a></td>
          <td>補 timing 軸 — #95 是 scope（橫向）、本卡是 timing（縱向）；兩軸都要對齊才完整</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></td>
          <td>補一條時機 — #83 把 multi-pass 描述成「N 輪 frame」、本卡點出「N 輪要分散在生成時間軸」、不是全集中 batch 後</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hook / linter 全綠但批量讀完感覺品質下滑</td>
          <td>Emergence 違規、改 stage 內抽樣機制</td>
      </tr>
      <tr>
          <td>Reviewer 報告抓到大量同類問題且都集中在 batch 末段</td>
          <td>Review 時機太晚、移到生成中</td>
      </tr>
      <tr>
          <td>想加新 lint rule 解決 cadence 問題</td>
          <td>警訊 — regex 攔不到、改 stage 內抽樣</td>
      </tr>
      <tr>
          <td>同 batch 修正 PR 改動 ≥ 20% 檔</td>
          <td>Stage 3 才發現 emergence、預設下一批要加 stage 2 抽樣</td>
      </tr>
      <tr>
          <td>「寫完 N 篇後抽樣」的 N 跟 batch size 同數量級</td>
          <td>抽樣等於 batch 後 review、N 應該 ≤ batch size × 20%</td>
      </tr>
      <tr>
          <td>寫作流程沒有「checkpoint」概念</td>
          <td>Enforcement 缺生成中這層、emergence 違規會持續產生</td>
      </tr>
      <tr>
          <td>Reviewer 只跑單檔 frame</td>
          <td>跨檔 emergence 看不到、補 reviewer prompt 要求跨檔比對</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：違規分字面 / 結構 / emergence 三類、enforcement 時機要對應類型。Emergence 違規規則化不了、不能丟給 hook 或 batch 後 reviewer、要在生成中（batch 進度 10-20% 時）抽樣 catch；最佳時機是 emergence 訊號剛夠強、且修正成本還可控的位置。</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><item><title>公開案例量是 vendor 社群活躍度 signal</title><link>https://tarrragon.github.io/blog/report/public-case-availability-vendor-signal/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/public-case-availability-vendor-signal/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>公開 customer engineering case 的累積量、是 vendor 社群活躍度跟長期可維護性的信號。case 多寡跟 vendor 工程能力沒有線性關係、跟以下因素相關：&lt;/p>
&lt;ul>
&lt;li>社群活躍度（用戶數 + 用戶寫 blog 文化）&lt;/li>
&lt;li>Vendor 自身的 customer success / DevRel 投入&lt;/li>
&lt;li>Feature 成熟度（新 feature 公開 case 通常稀薄）&lt;/li>
&lt;li>議題公開度（內部運維議題公司不常寫、incident / migration 容易寫）&lt;/li>
&lt;/ul>
&lt;p>選型時、公開 case 量值得作為信號之一、但要跟「該 vendor 是否仍積極開發」「文檔品質」「社群 issue 回應速度」等其他信號合併判讀。&lt;/p>
&lt;h2 id="為什麼">為什麼&lt;/h2>
&lt;p>backend/03-message-queue 模組 6 vendor 案例採集發現 case 累積量差異極大：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Vendor&lt;/th>
 &lt;th>採集前案例&lt;/th>
 &lt;th>公開可採集案例（5-10 目標）&lt;/th>
 &lt;th>累積差異&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Kafka&lt;/td>
 &lt;td>已有 8 個&lt;/td>
 &lt;td>12 個新案例（容易找）&lt;/td>
 &lt;td>案例豐富&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>RabbitMQ&lt;/td>
 &lt;td>0（待補）&lt;/td>
 &lt;td>11 個新案例&lt;/td>
 &lt;td>中等豐富&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS SQS&lt;/td>
 &lt;td>0（待補）&lt;/td>
 &lt;td>12 個新案例&lt;/td>
 &lt;td>豐富（managed service 客戶多）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Google Pub/Sub&lt;/td>
 &lt;td>0（待補）&lt;/td>
 &lt;td>10 個新案例（Mercari/Spotify 集中）&lt;/td>
 &lt;td>中等&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>NATS&lt;/td>
 &lt;td>0（待補）&lt;/td>
 &lt;td>8 個新案例（部分依 Synadia partner blog）&lt;/td>
 &lt;td>中等偏少&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Redis Streams&lt;/td>
 &lt;td>0（待補）&lt;/td>
 &lt;td>6 個新案例（不少公司用 Redis 但少寫 Streams）&lt;/td>
 &lt;td>偏少&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差異不只是「採集力度」、是公開資料密度本身差異。&lt;/p>
&lt;h2 id="反模式">反模式&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>「Kafka case 比 NATS 多、所以選 Kafka」&lt;/td>
 &lt;td>把 case 量當技術品質訊號、忽略需求形狀對齊（NATS 對 microservices messaging 可能更合適）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「Redis Streams case 少、所以不該用」&lt;/td>
 &lt;td>把案例稀薄當不成熟訊號、但 Redis Streams 在 Redis 生態內已是常見 pattern、只是公司不常單獨寫 blog&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「Pub/Sub case 集中在 Spotify + Mercari、所以代表性不足」&lt;/td>
 &lt;td>大公司多篇深度 case 比中等公司零散 case 教學價值更高、累積量不等於覆蓋廣度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="修法">修法&lt;/h2>
&lt;p>選型時把案例量當合併信號之一、跟以下信號交叉判讀：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>議題對齊度&lt;/strong>：該 vendor 的 case 是否覆蓋你的需求形狀（吞吐 / 延遲 / 持久化 / 多租戶 / 跨區）？&lt;/li>
&lt;li>&lt;strong>Vendor 活躍度&lt;/strong>：GitHub release 節奏、issue 回應速度、CVE 修復時間&lt;/li>
&lt;li>&lt;strong>生態整合&lt;/strong>：是否有你需要的 client library / framework / observability 工具&lt;/li>
&lt;li>&lt;strong>社群健康&lt;/strong>：Stack Overflow 問題回答率、Discord / Slack 活躍度&lt;/li>
&lt;li>&lt;strong>長期承諾&lt;/strong>：vendor 公司 / 基金會背景、license 模式、商業化路徑&lt;/li>
&lt;/ol>
&lt;p>單看案例量會誤導、但&lt;strong>完全忽略也會錯失重要信號&lt;/strong>：某些 vendor 案例量低反映社群活躍度低、選型後遇到問題找不到參考、自己要從零摸索。&lt;/p>
&lt;h2 id="關係">關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>跟採集流程的關係&lt;/strong>：採集到「該 vendor 公開 case 偏少」是真實信號、不是採集失敗、不該強求 10 個案例&lt;/li>
&lt;li>&lt;strong>跟 case-driven 寫作的關係&lt;/strong>：公開 case 稀薄的章節改走 standard-driven 或通用工程知識補強、明示覆蓋缺口&lt;/li>
&lt;li>&lt;strong>跟 vendor 選型的關係&lt;/strong>：案例量是合併信號之一、不是主要判讀依據&lt;/li>
&lt;/ul>
&lt;h2 id="case">case&lt;/h2>
&lt;p>backend/03-message-queue 模組採集後盤點：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>公開 customer engineering case 的累積量、是 vendor 社群活躍度跟長期可維護性的信號。case 多寡跟 vendor 工程能力沒有線性關係、跟以下因素相關：</p>
<ul>
<li>社群活躍度（用戶數 + 用戶寫 blog 文化）</li>
<li>Vendor 自身的 customer success / DevRel 投入</li>
<li>Feature 成熟度（新 feature 公開 case 通常稀薄）</li>
<li>議題公開度（內部運維議題公司不常寫、incident / migration 容易寫）</li>
</ul>
<p>選型時、公開 case 量值得作為信號之一、但要跟「該 vendor 是否仍積極開發」「文檔品質」「社群 issue 回應速度」等其他信號合併判讀。</p>
<h2 id="為什麼">為什麼</h2>
<p>backend/03-message-queue 模組 6 vendor 案例採集發現 case 累積量差異極大：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>採集前案例</th>
          <th>公開可採集案例（5-10 目標）</th>
          <th>累積差異</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kafka</td>
          <td>已有 8 個</td>
          <td>12 個新案例（容易找）</td>
          <td>案例豐富</td>
      </tr>
      <tr>
          <td>RabbitMQ</td>
          <td>0（待補）</td>
          <td>11 個新案例</td>
          <td>中等豐富</td>
      </tr>
      <tr>
          <td>AWS SQS</td>
          <td>0（待補）</td>
          <td>12 個新案例</td>
          <td>豐富（managed service 客戶多）</td>
      </tr>
      <tr>
          <td>Google Pub/Sub</td>
          <td>0（待補）</td>
          <td>10 個新案例（Mercari/Spotify 集中）</td>
          <td>中等</td>
      </tr>
      <tr>
          <td>NATS</td>
          <td>0（待補）</td>
          <td>8 個新案例（部分依 Synadia partner blog）</td>
          <td>中等偏少</td>
      </tr>
      <tr>
          <td>Redis Streams</td>
          <td>0（待補）</td>
          <td>6 個新案例（不少公司用 Redis 但少寫 Streams）</td>
          <td>偏少</td>
      </tr>
  </tbody>
</table>
<p>差異不只是「採集力度」、是公開資料密度本身差異。</p>
<h2 id="反模式">反模式</h2>
<p>選型時誤用案例量的方式：</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「Kafka case 比 NATS 多、所以選 Kafka」</td>
          <td>把 case 量當技術品質訊號、忽略需求形狀對齊（NATS 對 microservices messaging 可能更合適）</td>
      </tr>
      <tr>
          <td>「Redis Streams case 少、所以不該用」</td>
          <td>把案例稀薄當不成熟訊號、但 Redis Streams 在 Redis 生態內已是常見 pattern、只是公司不常單獨寫 blog</td>
      </tr>
      <tr>
          <td>「Pub/Sub case 集中在 Spotify + Mercari、所以代表性不足」</td>
          <td>大公司多篇深度 case 比中等公司零散 case 教學價值更高、累積量不等於覆蓋廣度</td>
      </tr>
  </tbody>
</table>
<h2 id="修法">修法</h2>
<p>選型時把案例量當合併信號之一、跟以下信號交叉判讀：</p>
<ol>
<li><strong>議題對齊度</strong>：該 vendor 的 case 是否覆蓋你的需求形狀（吞吐 / 延遲 / 持久化 / 多租戶 / 跨區）？</li>
<li><strong>Vendor 活躍度</strong>：GitHub release 節奏、issue 回應速度、CVE 修復時間</li>
<li><strong>生態整合</strong>：是否有你需要的 client library / framework / observability 工具</li>
<li><strong>社群健康</strong>：Stack Overflow 問題回答率、Discord / Slack 活躍度</li>
<li><strong>長期承諾</strong>：vendor 公司 / 基金會背景、license 模式、商業化路徑</li>
</ol>
<p>單看案例量會誤導、但<strong>完全忽略也會錯失重要信號</strong>：某些 vendor 案例量低反映社群活躍度低、選型後遇到問題找不到參考、自己要從零摸索。</p>
<h2 id="關係">關係</h2>
<ul>
<li><strong>跟採集流程的關係</strong>：採集到「該 vendor 公開 case 偏少」是真實信號、不是採集失敗、不該強求 10 個案例</li>
<li><strong>跟 case-driven 寫作的關係</strong>：公開 case 稀薄的章節改走 standard-driven 或通用工程知識補強、明示覆蓋缺口</li>
<li><strong>跟 vendor 選型的關係</strong>：案例量是合併信號之一、不是主要判讀依據</li>
</ul>
<h2 id="case">case</h2>
<p>backend/03-message-queue 模組採集後盤點：</p>
<ul>
<li>Kafka 17+ 案例、議題覆蓋廣度高、但 KRaft / 部分新 feature 仍稀薄</li>
<li>NATS 8 案例、議題集中在 IoT / edge / multi-cloud、其他場景偏少</li>
<li>Redis Streams 6 案例、Stream + Functions / Cluster on Streams 缺、是 feature 成熟度信號</li>
<li>Pub/Sub Mercari 4 篇深度 case 是 anchor cluster、品質高過案例量</li>
</ul>
<p>選型時把這些差異當輔助信號、不當主判讀。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>何時案例量該升為主要選型信號：</p>
<ul>
<li>該領域有很多 vendor 都做類似功能（如 message broker 有 7+ 個 vendor）、案例量可以區分活躍度</li>
<li>該 vendor 是新興 / 商業化不確定（vendor lock-in 風險）、需要評估社群獨立性</li>
<li>該 vendor 過去有 license 改變或商業化轉向（Redis / Elasticsearch / MongoDB）、社群 fork 的活躍度該追蹤</li>
</ul>
<p>何時案例量不該當主要信號：</p>
<ul>
<li>需求形狀已有明確 vendor 對齊（如 GCP 生態下 Pub/Sub 是預設）</li>
<li>Vendor 公司本身極穩定（AWS / Google managed service）</li>
<li>主要 case 集中在反例 / 退場案例（這時案例多反而是負面信號）</li>
</ul>
]]></content:encoded></item><item><title>教材目標先於決策框架</title><link>https://tarrragon.github.io/blog/report/teaching-goal-before-decision-frame/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/teaching-goal-before-decision-frame/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>教材目標要先於決策框架。教學內容的責任是讓讀者建立某個領域的心智模型、操作語意、常見壓力與演進路徑；決策框架的責任是幫讀者在理解過程中抓住風險、成本與取捨。&lt;/p>
&lt;p>決策框架是教學工具，教材目標是上位目的。當一套 Backend 教材被描述成「服務能力、風險、成本與決策」時，這個描述能提醒作者補足必要判準；上位目標仍要明確寫成「教讀者理解後端服務如何運作、如何組合、如何演進」。&lt;/p>
&lt;h2 id="warp-分析摘要">WARP 分析摘要&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>Anchor&lt;/td>
 &lt;td>這次問題的錨點從「Backend 是否有足夠決策內容」上移到「Backend 是否完成教學設計」。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Step 0&lt;/td>
 &lt;td>現有資料足以判斷：Backend 已有大量內容、案例、vendor index 與 migration playbook，但主入口仍偏能力地圖與決策語言。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Widen&lt;/td>
 &lt;td>選項有三種：繼續寫服務選型、繼續寫 vendor / migration、先補教材目標與學習路線。第三種最貼合教學錨點。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reality Test&lt;/td>
 &lt;td>對照 LLM 與 Go 目錄，成熟教材都先說讀者要學會什麼，再安排心智模型、工具、實作與延伸。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prepare&lt;/td>
 &lt;td>若後續 Backend 文章持續以「如何決策」起手，要用本卡重評估是否把教學目標降級成 governance frame。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向驗證：決策框架仍然必要。後端服務本身牽涉資料一致性、成本、事故與資安，教材需要同時講概念與取捨，讀者才有可操作判準。本卡的重點是層級排序：教材目標在上，決策框架在內。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>Backend 內容已累積到一定規模後，評估方向時容易把現有語言當成目標本身。前一輪評估把 Backend 的原始定位概括成「服務能力、風險、成本與決策」，這個概括抓到了內容中的重要維度，但讀者指出更準確的定位是「教學」。&lt;/p>
&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;/td>
 &lt;td>讀者學完後，是否理解後端服務如何共同支撐 production system？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>決策框架&lt;/td>
 &lt;td>讀者理解每個服務時，是否知道能力、風險、成本與取捨？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第一層決定教材方向，第二層服務第一層。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="先寫教材要讓讀者學會什麼">先寫教材要讓讀者學會什麼&lt;/h3>
&lt;p>教材入口要先回答讀者學習成果。Backend 的學習成果應寫成「能看懂一個後端服務問題該交給哪類能力處理，並理解多個能力如何串接」；Redis / Kafka / Kubernetes 的優缺點比較則放在這個學習成果之下。&lt;/p>
&lt;p>較穩定的教材目標句是：&lt;/p>
&lt;blockquote>
&lt;p>Backend 教材教讀者理解後端服務如何承擔資料、流量、交接、觀測、部署、可靠性、資安、事故與容量責任，並學會把這些能力組合成可操作、可演進的 production system。&lt;/p>&lt;/blockquote>
&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;/td>
 &lt;td>讓讀者知道這類服務負責哪段系統責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>風險&lt;/td>
 &lt;td>讓讀者知道錯用或失效時會造成什麼後果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本&lt;/td>
 &lt;td>讓讀者知道引入服務後誰要維護、支付與驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>決策&lt;/td>
 &lt;td>讓讀者知道何時選、何時不選、何時回退&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="用內容結構保護教學目標">用內容結構保護教學目標&lt;/h3>
&lt;p>每個教學模組應先有學習路線，再有服務清單。服務清單是素材，學習路線才是教材。當文章先列 vendor、工具與 migration backlog，讀者會自然把教材理解成產品百科或選型資料庫。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="教材會變成決策備忘錄">教材會變成決策備忘錄&lt;/h3>
&lt;p>決策備忘錄能幫已經有背景的人判斷取捨，教材則要幫讀者建立概念。讀者看到大量「何時選 X」與「X 的成本」時，可能知道該問什麼問題，仍需要補上 X 在系統裡承擔什麼責任。&lt;/p>
&lt;h3 id="內容會被-vendor-與-migration-慣性帶走">內容會被 vendor 與 migration 慣性帶走&lt;/h3>
&lt;p>Vendor 頁與 migration playbook 很容易量產，因為每篇都有明確對象。教材目標放在上層時，內容會往「更清楚的學習梯度」收斂；教材目標缺位時，內容會自然往「更多服務、更多遷移」擴張。&lt;/p>
&lt;h3 id="讀者學習路線會被能力地圖取代">讀者學習路線會被能力地圖取代&lt;/h3>
&lt;p>能力地圖對作者有用，因為作者已知道整體結構。讀者需要的是先後順序：先理解什麼，再看什麼，遇到哪種問題跳到哪裡。缺少學習路線時，讀者會在能力地圖中自行找路，學習成本變高。&lt;/p>
&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>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關&lt;/a>&lt;/td>
 &lt;td>把教材目標寫成決策框架很方便，因為它抽象且好列欄位；但便利描述未必對齊「教學」意圖。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../prose-self-contained-without-code-reference/">#113 商業邏輯論述要 self-contained&lt;/a>&lt;/td>
 &lt;td>教材目標也要 self-contained；入口頁要直接說明 Backend 是教學，讓讀者在進入章節前取得學習錨點。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性&lt;/a>&lt;/td>
 &lt;td>教材目標是 review 的 Anchor 軸。若 review 只看內容覆蓋，會漏掉「這是否仍是教材」這個上位問題。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../content-structure-by-max-diff-dimension/">#127 Process content 結構由最大差異維度決定&lt;/a>&lt;/td>
 &lt;td>#127 說 process content 結構由差異維度決定；本卡補教材 content 的上位結構由學習目標決定。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>系列定位只剩能力、風險、成本、決策&lt;/td>
 &lt;td>重新寫教材目標句，先說讀者要學會什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>入口頁像服務清單或 vendor roadmap&lt;/td>
 &lt;td>補學習路線與讀者旅程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>每篇文章都能做取捨分析，但讀者不知道先讀哪篇&lt;/td>
 &lt;td>補模組梯度與貫穿式案例&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration / vendor backlog 持續增加，主線教學沒有變清楚&lt;/td>
 &lt;td>暫停量產，回到教材設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Review 只問「內容有沒有覆蓋」&lt;/td>
 &lt;td>加問「這段是否服務教材目標」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：教材要先定義學習成果，再放入決策框架。服務能力、風險、成本與決策是理解 Backend 的必要工具，但教材的上位目標是教讀者建立後端服務的心智模型與操作語意。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>教材目標要先於決策框架。教學內容的責任是讓讀者建立某個領域的心智模型、操作語意、常見壓力與演進路徑；決策框架的責任是幫讀者在理解過程中抓住風險、成本與取捨。</p>
<p>決策框架是教學工具，教材目標是上位目的。當一套 Backend 教材被描述成「服務能力、風險、成本與決策」時，這個描述能提醒作者補足必要判準；上位目標仍要明確寫成「教讀者理解後端服務如何運作、如何組合、如何演進」。</p>
<h2 id="warp-分析摘要">WARP 分析摘要</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anchor</td>
          <td>這次問題的錨點從「Backend 是否有足夠決策內容」上移到「Backend 是否完成教學設計」。</td>
      </tr>
      <tr>
          <td>Step 0</td>
          <td>現有資料足以判斷：Backend 已有大量內容、案例、vendor index 與 migration playbook，但主入口仍偏能力地圖與決策語言。</td>
      </tr>
      <tr>
          <td>Widen</td>
          <td>選項有三種：繼續寫服務選型、繼續寫 vendor / migration、先補教材目標與學習路線。第三種最貼合教學錨點。</td>
      </tr>
      <tr>
          <td>Reality Test</td>
          <td>對照 LLM 與 Go 目錄，成熟教材都先說讀者要學會什麼，再安排心智模型、工具、實作與延伸。</td>
      </tr>
      <tr>
          <td>Prepare</td>
          <td>若後續 Backend 文章持續以「如何決策」起手，要用本卡重評估是否把教學目標降級成 governance frame。</td>
      </tr>
  </tbody>
</table>
<p>反向驗證：決策框架仍然必要。後端服務本身牽涉資料一致性、成本、事故與資安，教材需要同時講概念與取捨，讀者才有可操作判準。本卡的重點是層級排序：教材目標在上，決策框架在內。</p>
<h2 id="情境">情境</h2>
<p>Backend 內容已累積到一定規模後，評估方向時容易把現有語言當成目標本身。前一輪評估把 Backend 的原始定位概括成「服務能力、風險、成本與決策」，這個概括抓到了內容中的重要維度，但讀者指出更準確的定位是「教學」。</p>
<p>這個修正揭露了兩層差異：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>教材目標</td>
          <td>讀者學完後，是否理解後端服務如何共同支撐 production system？</td>
      </tr>
      <tr>
          <td>決策框架</td>
          <td>讀者理解每個服務時，是否知道能力、風險、成本與取捨？</td>
      </tr>
  </tbody>
</table>
<p>第一層決定教材方向，第二層服務第一層。</p>
<h2 id="理想做法">理想做法</h2>
<h3 id="先寫教材要讓讀者學會什麼">先寫教材要讓讀者學會什麼</h3>
<p>教材入口要先回答讀者學習成果。Backend 的學習成果應寫成「能看懂一個後端服務問題該交給哪類能力處理，並理解多個能力如何串接」；Redis / Kafka / Kubernetes 的優缺點比較則放在這個學習成果之下。</p>
<p>較穩定的教材目標句是：</p>
<blockquote>
<p>Backend 教材教讀者理解後端服務如何承擔資料、流量、交接、觀測、部署、可靠性、資安、事故與容量責任，並學會把這些能力組合成可操作、可演進的 production system。</p></blockquote>
<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>
<h3 id="用內容結構保護教學目標">用內容結構保護教學目標</h3>
<p>每個教學模組應先有學習路線，再有服務清單。服務清單是素材，學習路線才是教材。當文章先列 vendor、工具與 migration backlog，讀者會自然把教材理解成產品百科或選型資料庫。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="教材會變成決策備忘錄">教材會變成決策備忘錄</h3>
<p>決策備忘錄能幫已經有背景的人判斷取捨，教材則要幫讀者建立概念。讀者看到大量「何時選 X」與「X 的成本」時，可能知道該問什麼問題，仍需要補上 X 在系統裡承擔什麼責任。</p>
<h3 id="內容會被-vendor-與-migration-慣性帶走">內容會被 vendor 與 migration 慣性帶走</h3>
<p>Vendor 頁與 migration playbook 很容易量產，因為每篇都有明確對象。教材目標放在上層時，內容會往「更清楚的學習梯度」收斂；教材目標缺位時，內容會自然往「更多服務、更多遷移」擴張。</p>
<h3 id="讀者學習路線會被能力地圖取代">讀者學習路線會被能力地圖取代</h3>
<p>能力地圖對作者有用，因為作者已知道整體結構。讀者需要的是先後順序：先理解什麼，再看什麼，遇到哪種問題跳到哪裡。缺少學習路線時，讀者會在能力地圖中自行找路，學習成本變高。</p>
<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>把教材目標寫成決策框架很方便，因為它抽象且好列欄位；但便利描述未必對齊「教學」意圖。</td>
      </tr>
      <tr>
          <td><a href="../prose-self-contained-without-code-reference/">#113 商業邏輯論述要 self-contained</a></td>
          <td>教材目標也要 self-contained；入口頁要直接說明 Backend 是教學，讓讀者在進入章節前取得學習錨點。</td>
      </tr>
      <tr>
          <td><a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性</a></td>
          <td>教材目標是 review 的 Anchor 軸。若 review 只看內容覆蓋，會漏掉「這是否仍是教材」這個上位問題。</td>
      </tr>
      <tr>
          <td><a href="../content-structure-by-max-diff-dimension/">#127 Process content 結構由最大差異維度決定</a></td>
          <td>#127 說 process content 結構由差異維度決定；本卡補教材 content 的上位結構由學習目標決定。</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>系列定位只剩能力、風險、成本、決策</td>
          <td>重新寫教材目標句，先說讀者要學會什麼</td>
      </tr>
      <tr>
          <td>入口頁像服務清單或 vendor roadmap</td>
          <td>補學習路線與讀者旅程</td>
      </tr>
      <tr>
          <td>每篇文章都能做取捨分析，但讀者不知道先讀哪篇</td>
          <td>補模組梯度與貫穿式案例</td>
      </tr>
      <tr>
          <td>Migration / vendor backlog 持續增加，主線教學沒有變清楚</td>
          <td>暫停量產，回到教材設計</td>
      </tr>
      <tr>
          <td>Review 只問「內容有沒有覆蓋」</td>
          <td>加問「這段是否服務教材目標」</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：教材要先定義學習成果，再放入決策框架。服務能力、風險、成本與決策是理解 Backend 的必要工具，但教材的上位目標是教讀者建立後端服務的心智模型與操作語意。</p>
]]></content:encoded></item><item><title>教材完整性要用讀者旅程驗證</title><link>https://tarrragon.github.io/blog/report/teaching-completeness-by-learner-journey/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/teaching-completeness-by-learner-journey/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>教材完整性要用讀者旅程驗證。章節數、案例數、vendor 頁數與 knowledge card 數量能證明素材充足；完成設計的教材還能回答不同讀者「我該從哪裡開始、按什麼順序讀、讀完能做什麼」。&lt;/p>
&lt;p>讀者旅程是教材的 routing layer。它把素材庫、主章、案例、實作與進階專題組成學習路線，讓讀者直接取得順序，而非從內容地圖中自行推導。&lt;/p>
&lt;h2 id="warp-分析摘要">WARP 分析摘要&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>Anchor&lt;/td>
 &lt;td>評估 Backend 是否完成教學設計，要看讀者能否找到學習路線，內容量只是前置條件。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Step 0&lt;/td>
 &lt;td>對照 LLM / Go / Go advanced，成熟入口都明確列出目標讀者、學習路線、模組分工與主題導讀。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Widen&lt;/td>
 &lt;td>可用三種指標：內容覆蓋、結構覆蓋、讀者旅程覆蓋。教材完整性應以第三種為主。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reality Test&lt;/td>
 &lt;td>Backend 目前有內容覆蓋與結構覆蓋，但讀者路線仍偏弱；LLM 與 Go 則能讓讀者依目的選路。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prepare&lt;/td>
 &lt;td>若後續新增章節後只能說出內容清單、說不出 3-5 條讀者路線，代表教材設計仍在素材整理階段。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向驗證：讀者旅程與完整目錄各有責任。完整目錄仍是查閱與維護入口；讀者旅程是學習入口。兩者分工是「目錄服務作者與回查，旅程服務學習」。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>Backend 內容已經具備大量材料：主章、案例庫、vendor 頁、migration playbook、knowledge cards 都很完整。若只用內容覆蓋判斷，很容易得到「教學內容已經完成」的結論。&lt;/p>
&lt;p>對照 LLM 與 Go 後，真正差異浮現：&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>LLM&lt;/td>
 &lt;td>依目的分路線：本地 Mac、PC GPU、理論、應用、安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Go&lt;/td>
 &lt;td>依學習梯度排序：哲學、基礎、型別、標準庫、並發、測試、實戰、重構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Go advanced&lt;/td>
 &lt;td>依角色分路線：並發服務維護者、WebSocket/API 開發者、效能與可靠性工程師&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backend&lt;/td>
 &lt;td>目前主要依能力分類：資料庫、快取、queue、觀測、部署、可靠性、資安、事故、效能&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Backend 的能力分類正確；下一層要補讀者旅程。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步先列讀者旅程">第一步：先列讀者旅程&lt;/h3>
&lt;p>教材入口應先列 3-5 條讀者路線，每條路線都要有起點、順序與完成狀態。&lt;/p>
&lt;p>Backend 可用的路線範例：&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>想理解 backend 服務分工&lt;/td>
 &lt;td>00 → knowledge cards → 01/02/03 概念章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API 到資料流&lt;/td>
 &lt;td>想設計 API 背後的 DB / cache / queue&lt;/td>
 &lt;td>01 → 02 → 03 → 04 evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production 操作&lt;/td>
 &lt;td>想學觀測、部署、可靠性與事故&lt;/td>
 &lt;td>04 → 05 → 06 → 08&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security / data protection&lt;/td>
 &lt;td>想理解權限、資料、偵測與回應&lt;/td>
 &lt;td>07 → 04 audit evidence → 08 security incident&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vendor / migration&lt;/td>
 &lt;td>已懂分類、要比較工具或遷移&lt;/td>
 &lt;td>對應 vendors/ → migration playbook → 案例&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些路線主要重組現有內容，把內容地圖整理成讀者可走的路。&lt;/p>
&lt;h3 id="第二步為每條路線定義讀完能做什麼">第二步：為每條路線定義讀完能做什麼&lt;/h3>
&lt;p>學習路線要有完成判準。只列「讀 A、讀 B、讀 C」仍像目錄；加上「讀完能判斷什麼」才是教學設計。&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>能把一個需求分類成 state、cache、queue、observability、deployment 或 reliability 問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API 到資料流&lt;/td>
 &lt;td>能說明一次 checkout 如何跨 DB、cache、queue 與 evidence package&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production 操作&lt;/td>
 &lt;td>能把 release、alert、gate、incident decision log 串成閉環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security / data protection&lt;/td>
 &lt;td>能從身份、資料、入口、秘密與 audit evidence 判讀控制面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vendor / migration&lt;/td>
 &lt;td>能先判斷分類責任，再比較具體服務與遷移成本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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;/td>
 &lt;td>缺少導讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路線跨模組時只靠零散 link&lt;/td>
 &lt;td>缺少串接章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路線完成後沒有實作或案例&lt;/td>
 &lt;td>缺少可操作收束&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>vendor / migration 入口早於分類心智模型&lt;/td>
 &lt;td>進階材料壓過主線&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="內容越多讀者越難開始">內容越多，讀者越難開始&lt;/h3>
&lt;p>素材量增加會讓作者覺得教材完整，但讀者面對大量章節時需要的是順序。沒有讀者旅程時，內容量越大，入口成本越高。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>教材完整性要用讀者旅程驗證。章節數、案例數、vendor 頁數與 knowledge card 數量能證明素材充足；完成設計的教材還能回答不同讀者「我該從哪裡開始、按什麼順序讀、讀完能做什麼」。</p>
<p>讀者旅程是教材的 routing layer。它把素材庫、主章、案例、實作與進階專題組成學習路線，讓讀者直接取得順序，而非從內容地圖中自行推導。</p>
<h2 id="warp-分析摘要">WARP 分析摘要</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anchor</td>
          <td>評估 Backend 是否完成教學設計，要看讀者能否找到學習路線，內容量只是前置條件。</td>
      </tr>
      <tr>
          <td>Step 0</td>
          <td>對照 LLM / Go / Go advanced，成熟入口都明確列出目標讀者、學習路線、模組分工與主題導讀。</td>
      </tr>
      <tr>
          <td>Widen</td>
          <td>可用三種指標：內容覆蓋、結構覆蓋、讀者旅程覆蓋。教材完整性應以第三種為主。</td>
      </tr>
      <tr>
          <td>Reality Test</td>
          <td>Backend 目前有內容覆蓋與結構覆蓋，但讀者路線仍偏弱；LLM 與 Go 則能讓讀者依目的選路。</td>
      </tr>
      <tr>
          <td>Prepare</td>
          <td>若後續新增章節後只能說出內容清單、說不出 3-5 條讀者路線，代表教材設計仍在素材整理階段。</td>
      </tr>
  </tbody>
</table>
<p>反向驗證：讀者旅程與完整目錄各有責任。完整目錄仍是查閱與維護入口；讀者旅程是學習入口。兩者分工是「目錄服務作者與回查，旅程服務學習」。</p>
<h2 id="情境">情境</h2>
<p>Backend 內容已經具備大量材料：主章、案例庫、vendor 頁、migration playbook、knowledge cards 都很完整。若只用內容覆蓋判斷，很容易得到「教學內容已經完成」的結論。</p>
<p>對照 LLM 與 Go 後，真正差異浮現：</p>
<table>
  <thead>
      <tr>
          <th>教材</th>
          <th>完整性訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>LLM</td>
          <td>依目的分路線：本地 Mac、PC GPU、理論、應用、安全</td>
      </tr>
      <tr>
          <td>Go</td>
          <td>依學習梯度排序：哲學、基礎、型別、標準庫、並發、測試、實戰、重構</td>
      </tr>
      <tr>
          <td>Go advanced</td>
          <td>依角色分路線：並發服務維護者、WebSocket/API 開發者、效能與可靠性工程師</td>
      </tr>
      <tr>
          <td>Backend</td>
          <td>目前主要依能力分類：資料庫、快取、queue、觀測、部署、可靠性、資安、事故、效能</td>
      </tr>
  </tbody>
</table>
<p>Backend 的能力分類正確；下一層要補讀者旅程。</p>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步先列讀者旅程">第一步：先列讀者旅程</h3>
<p>教材入口應先列 3-5 條讀者路線，每條路線都要有起點、順序與完成狀態。</p>
<p>Backend 可用的路線範例：</p>
<table>
  <thead>
      <tr>
          <th>路線</th>
          <th>適合讀者</th>
          <th>閱讀順序</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>系統心智模型</td>
          <td>想理解 backend 服務分工</td>
          <td>00 → knowledge cards → 01/02/03 概念章</td>
      </tr>
      <tr>
          <td>API 到資料流</td>
          <td>想設計 API 背後的 DB / cache / queue</td>
          <td>01 → 02 → 03 → 04 evidence</td>
      </tr>
      <tr>
          <td>Production 操作</td>
          <td>想學觀測、部署、可靠性與事故</td>
          <td>04 → 05 → 06 → 08</td>
      </tr>
      <tr>
          <td>Security / data protection</td>
          <td>想理解權限、資料、偵測與回應</td>
          <td>07 → 04 audit evidence → 08 security incident</td>
      </tr>
      <tr>
          <td>Vendor / migration</td>
          <td>已懂分類、要比較工具或遷移</td>
          <td>對應 vendors/ → migration playbook → 案例</td>
      </tr>
  </tbody>
</table>
<p>這些路線主要重組現有內容，把內容地圖整理成讀者可走的路。</p>
<h3 id="第二步為每條路線定義讀完能做什麼">第二步：為每條路線定義讀完能做什麼</h3>
<p>學習路線要有完成判準。只列「讀 A、讀 B、讀 C」仍像目錄；加上「讀完能判斷什麼」才是教學設計。</p>
<table>
  <thead>
      <tr>
          <th>路線</th>
          <th>完成判準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>系統心智模型</td>
          <td>能把一個需求分類成 state、cache、queue、observability、deployment 或 reliability 問題</td>
      </tr>
      <tr>
          <td>API 到資料流</td>
          <td>能說明一次 checkout 如何跨 DB、cache、queue 與 evidence package</td>
      </tr>
      <tr>
          <td>Production 操作</td>
          <td>能把 release、alert、gate、incident decision log 串成閉環</td>
      </tr>
      <tr>
          <td>Security / data protection</td>
          <td>能從身份、資料、入口、秘密與 audit evidence 判讀控制面</td>
      </tr>
      <tr>
          <td>Vendor / migration</td>
          <td>能先判斷分類責任，再比較具體服務與遷移成本</td>
      </tr>
  </tbody>
</table>
<h3 id="第三步用旅程檢查內容缺口">第三步：用旅程檢查內容缺口</h3>
<p>內容缺口要從旅程反推。若某條路線中間需要讀者自己跳轉，就代表教材設計有洞。</p>
<table>
  <thead>
      <tr>
          <th>缺口訊號</th>
          <th>代表問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>路線需要讀者自己從十幾篇找下一篇</td>
          <td>缺少導讀</td>
      </tr>
      <tr>
          <td>路線跨模組時只靠零散 link</td>
          <td>缺少串接章</td>
      </tr>
      <tr>
          <td>路線完成後沒有實作或案例</td>
          <td>缺少可操作收束</td>
      </tr>
      <tr>
          <td>vendor / migration 入口早於分類心智模型</td>
          <td>進階材料壓過主線</td>
      </tr>
  </tbody>
</table>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="內容越多讀者越難開始">內容越多，讀者越難開始</h3>
<p>素材量增加會讓作者覺得教材完整，但讀者面對大量章節時需要的是順序。沒有讀者旅程時，內容量越大，入口成本越高。</p>
<h3 id="作者會誤把-backlog-完成當成教材完成">作者會誤把 backlog 完成當成教材完成</h3>
<p>Vendor backlog、migration backlog 與案例庫都有清楚項目，很容易形成完成感。教學完成感應該來自「讀者能走完一條路線」；作者清單完成只是素材面的進度。</p>
<h3 id="review-會偏向覆蓋率不看學習梯度">Review 會偏向覆蓋率，不看學習梯度</h3>
<p>審查時若只問「哪些主題還沒寫」，會自然補更多主題。讀者旅程 frame 會改問「這些主題排列後，讀者是否能從低負擔走到高負擔」。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../cards-as-living-system-iteration/">#81 卡片系統的迭代浮現</a></td>
          <td>讀者旅程是卡片 / 主章 / 案例累積後浮現的 meta-layer，不應在素材不足時硬設，也不應在素材充足後缺席。</td>
      </tr>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></td>
          <td>讀者旅程是系列級 metadata surface；它是讀者進入內容前最先看到的 routing 文字。</td>
      </tr>
      <tr>
          <td><a href="../routing-layer-chapter-recognition/">#119 章節已有 routing skeleton 走補強段</a></td>
          <td>本卡把 routing layer 從單章提升到系列入口。系列入口已有能力地圖時，下一步是補旅程路由。</td>
      </tr>
      <tr>
          <td><a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性</a></td>
          <td>教材 review 要新增 learner journey 軸；surface、scope、cadence 之外還要檢查教材是否能被學習。</td>
      </tr>
      <tr>
          <td><a href="../teaching-goal-before-decision-frame/">#130 教材目標先於決策框架</a></td>
          <td>#130 定義上位目標，本卡定義如何驗證該目標是否落到讀者路線。</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>入口頁能列完整模組，讀者路線仍缺席</td>
          <td>補「學習路線」段</td>
      </tr>
      <tr>
          <td>讀者需要自己判斷先看主章、案例還是 vendor</td>
          <td>補起點與完成判準</td>
      </tr>
      <tr>
          <td>章節很多但沒有「讀完能做什麼」</td>
          <td>補每條路線的學習成果</td>
      </tr>
      <tr>
          <td>新增內容多集中在 backlog，而非導讀</td>
          <td>暫停新增素材，先整理 routing</td>
      </tr>
      <tr>
          <td>對照成熟教材時，只能對到目錄，對不到旅程</td>
          <td>教學設計尚未完成</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：教材完整性由讀者旅程驗證，內容覆蓋率只是一個輸入。成熟教材能讓不同目的的讀者找到起點、順序與完成判準；內容地圖服務查閱，讀者旅程服務學習。</p>
]]></content:encoded></item><item><title>貫穿式案例是服務教材的教學骨架</title><link>https://tarrragon.github.io/blog/report/throughline-case-as-teaching-spine/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/throughline-case-as-teaching-spine/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>服務型教材需要貫穿式案例作為教學骨架。資料庫、快取、queue、觀測、部署、可靠性、資安、事故與容量都可以獨立成章，但讀者真正需要學會的是這些能力如何在同一個服務裡交接、互相約束並共同演進。&lt;/p>
&lt;p>貫穿式案例是一條可重播的服務演進路徑，而非單一大型專案手冊。它用同一個中性服務情境反覆穿過多個模組，讓讀者看到每個模組處理的是同一個系統在不同壓力下的責任切面。&lt;/p>
&lt;h2 id="warp-分析摘要">WARP 分析摘要&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>Anchor&lt;/td>
 &lt;td>Backend 教材要教的是後端服務如何共同支撐 production system，單章正確不足以證明整體教學成立。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Step 0&lt;/td>
 &lt;td>現有 Backend 已有多個服務路徑示範與 artifact backbone，但還缺系列入口層明示的貫穿式案例。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Widen&lt;/td>
 &lt;td>可選方式有能力分類、讀者旅程、貫穿式案例。三者可疊加：能力分類是目錄，讀者旅程是路線，貫穿式案例是演練骨架。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reality Test&lt;/td>
 &lt;td>Go 用簡化通知服務承接語法到實戰，LLM 用本地 LLM 工作流承接心智模型到工具；Backend 也需要同類骨架。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prepare&lt;/td>
 &lt;td>若後續章節各自引用不同情境，讀者仍難以看出 DB / cache / queue / observability / incident 如何在同一服務內交接。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向驗證：貫穿式案例要維持中性、簡化、可替換，並明示它是教學載體；讀者理解案例所需的背景要由文章提供，而非內部專案知識。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>Backend 已有多篇服務路徑示範，例如 schema migration evidence、cache migration rollback、queue retry replay、checkout API evidence package、release gate、credential rotation 與 incident decision log。這些文章各自能說明一段能力，但它們在入口層還沒有被明確收斂成一條「讀者可以跟著走」的服務演進路線。&lt;/p>
&lt;p>對照 Go 與 LLM：&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>Go&lt;/td>
 &lt;td>從小程式走到簡化即時通知服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Go advanced&lt;/td>
 &lt;td>用長時間運行服務、WebSocket、event-driven service 當重複情境&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>LLM&lt;/td>
 &lt;td>用本地 LLM 寫 code 工作流，把硬體、推論伺服器、模型、IDE、安全串起來&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backend&lt;/td>
 &lt;td>目前多個 artifact 示範分散存在，尚未在入口層組成一條主案例&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Backend 的內容特性更需要貫穿式案例，因為它處理的是多個外部服務的協作教材，範圍大於單一語言或單一工具。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步選一個中性服務作為載體">第一步：選一個中性服務作為載體&lt;/h3>
&lt;p>貫穿式案例應該選讀者容易理解、又能自然觸發多個 Backend 模組的服務。較穩定的候選是 &lt;code>checkout / order / payment / notification&lt;/code> 類流程。&lt;/p>
&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>01 Database&lt;/td>
 &lt;td>order / payment 狀態、schema migration、reconciliation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>02 Cache&lt;/td>
 &lt;td>商品、價格或 entitlement 的 freshness 與 origin protection&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>03 Queue&lt;/td>
 &lt;td>order_created / payment_confirmed 的 retry、DLQ、replay&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>04 Observability&lt;/td>
 &lt;td>checkout evidence package、trace、dashboard、query link&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>05 Deployment&lt;/td>
 &lt;td>checkout service rollout、drain、rollback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>06 Reliability&lt;/td>
 &lt;td>provider dependency release gate、load / chaos / regression&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>07 Security&lt;/td>
 &lt;td>webhook secret rotation、PII masking、audit evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>08 Incident&lt;/td>
 &lt;td>payment incident decision log、write-back、action item&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>09 Performance&lt;/td>
 &lt;td>peak checkout capacity、saturation、cost per request&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="第二步把案例拆成多個可重播-episode">第二步：把案例拆成多個可重播 episode&lt;/h3>
&lt;p>貫穿式案例要避免寫成一篇巨文。較穩定的做法是拆成 episode，每個 episode 對應一個模組責任。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Episode&lt;/th>
 &lt;th>問題&lt;/th>
 &lt;th>主要模組&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>E1&lt;/td>
 &lt;td>新增付款狀態欄位&lt;/td>
 &lt;td>01 + 04 + 08&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E2&lt;/td>
 &lt;td>商品價格快取失效與回源保護&lt;/td>
 &lt;td>02 + 04 + 06&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E3&lt;/td>
 &lt;td>訂單事件 consumer 失敗與 replay&lt;/td>
 &lt;td>03 + 06 + 08&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E4&lt;/td>
 &lt;td>Checkout service rollout&lt;/td>
 &lt;td>05 + 04 + 08&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E5&lt;/td>
 &lt;td>Payment provider timeout 變更&lt;/td>
 &lt;td>06 + 04 + 09&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E6&lt;/td>
 &lt;td>Webhook secret rotation&lt;/td>
 &lt;td>07 + 04 + 08&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>E7&lt;/td>
 &lt;td>Flash-sale peak readiness&lt;/td>
 &lt;td>09 + 02 + 03 + 06&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Episode 讓讀者看到「同一服務在不同壓力下需要不同模組」，同時保留單篇文章的原子性。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>服務型教材需要貫穿式案例作為教學骨架。資料庫、快取、queue、觀測、部署、可靠性、資安、事故與容量都可以獨立成章，但讀者真正需要學會的是這些能力如何在同一個服務裡交接、互相約束並共同演進。</p>
<p>貫穿式案例是一條可重播的服務演進路徑，而非單一大型專案手冊。它用同一個中性服務情境反覆穿過多個模組，讓讀者看到每個模組處理的是同一個系統在不同壓力下的責任切面。</p>
<h2 id="warp-分析摘要">WARP 分析摘要</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anchor</td>
          <td>Backend 教材要教的是後端服務如何共同支撐 production system，單章正確不足以證明整體教學成立。</td>
      </tr>
      <tr>
          <td>Step 0</td>
          <td>現有 Backend 已有多個服務路徑示範與 artifact backbone，但還缺系列入口層明示的貫穿式案例。</td>
      </tr>
      <tr>
          <td>Widen</td>
          <td>可選方式有能力分類、讀者旅程、貫穿式案例。三者可疊加：能力分類是目錄，讀者旅程是路線，貫穿式案例是演練骨架。</td>
      </tr>
      <tr>
          <td>Reality Test</td>
          <td>Go 用簡化通知服務承接語法到實戰，LLM 用本地 LLM 工作流承接心智模型到工具；Backend 也需要同類骨架。</td>
      </tr>
      <tr>
          <td>Prepare</td>
          <td>若後續章節各自引用不同情境，讀者仍難以看出 DB / cache / queue / observability / incident 如何在同一服務內交接。</td>
      </tr>
  </tbody>
</table>
<p>反向驗證：貫穿式案例要維持中性、簡化、可替換，並明示它是教學載體；讀者理解案例所需的背景要由文章提供，而非內部專案知識。</p>
<h2 id="情境">情境</h2>
<p>Backend 已有多篇服務路徑示範，例如 schema migration evidence、cache migration rollback、queue retry replay、checkout API evidence package、release gate、credential rotation 與 incident decision log。這些文章各自能說明一段能力，但它們在入口層還沒有被明確收斂成一條「讀者可以跟著走」的服務演進路線。</p>
<p>對照 Go 與 LLM：</p>
<table>
  <thead>
      <tr>
          <th>教材</th>
          <th>貫穿骨架</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go</td>
          <td>從小程式走到簡化即時通知服務</td>
      </tr>
      <tr>
          <td>Go advanced</td>
          <td>用長時間運行服務、WebSocket、event-driven service 當重複情境</td>
      </tr>
      <tr>
          <td>LLM</td>
          <td>用本地 LLM 寫 code 工作流，把硬體、推論伺服器、模型、IDE、安全串起來</td>
      </tr>
      <tr>
          <td>Backend</td>
          <td>目前多個 artifact 示範分散存在，尚未在入口層組成一條主案例</td>
      </tr>
  </tbody>
</table>
<p>Backend 的內容特性更需要貫穿式案例，因為它處理的是多個外部服務的協作教材，範圍大於單一語言或單一工具。</p>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步選一個中性服務作為載體">第一步：選一個中性服務作為載體</h3>
<p>貫穿式案例應該選讀者容易理解、又能自然觸發多個 Backend 模組的服務。較穩定的候選是 <code>checkout / order / payment / notification</code> 類流程。</p>
<p>這條服務路徑可承接：</p>
<table>
  <thead>
      <tr>
          <th>模組</th>
          <th>在案例中的責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>01 Database</td>
          <td>order / payment 狀態、schema migration、reconciliation</td>
      </tr>
      <tr>
          <td>02 Cache</td>
          <td>商品、價格或 entitlement 的 freshness 與 origin protection</td>
      </tr>
      <tr>
          <td>03 Queue</td>
          <td>order_created / payment_confirmed 的 retry、DLQ、replay</td>
      </tr>
      <tr>
          <td>04 Observability</td>
          <td>checkout evidence package、trace、dashboard、query link</td>
      </tr>
      <tr>
          <td>05 Deployment</td>
          <td>checkout service rollout、drain、rollback</td>
      </tr>
      <tr>
          <td>06 Reliability</td>
          <td>provider dependency release gate、load / chaos / regression</td>
      </tr>
      <tr>
          <td>07 Security</td>
          <td>webhook secret rotation、PII masking、audit evidence</td>
      </tr>
      <tr>
          <td>08 Incident</td>
          <td>payment incident decision log、write-back、action item</td>
      </tr>
      <tr>
          <td>09 Performance</td>
          <td>peak checkout capacity、saturation、cost per request</td>
      </tr>
  </tbody>
</table>
<h3 id="第二步把案例拆成多個可重播-episode">第二步：把案例拆成多個可重播 episode</h3>
<p>貫穿式案例要避免寫成一篇巨文。較穩定的做法是拆成 episode，每個 episode 對應一個模組責任。</p>
<table>
  <thead>
      <tr>
          <th>Episode</th>
          <th>問題</th>
          <th>主要模組</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>E1</td>
          <td>新增付款狀態欄位</td>
          <td>01 + 04 + 08</td>
      </tr>
      <tr>
          <td>E2</td>
          <td>商品價格快取失效與回源保護</td>
          <td>02 + 04 + 06</td>
      </tr>
      <tr>
          <td>E3</td>
          <td>訂單事件 consumer 失敗與 replay</td>
          <td>03 + 06 + 08</td>
      </tr>
      <tr>
          <td>E4</td>
          <td>Checkout service rollout</td>
          <td>05 + 04 + 08</td>
      </tr>
      <tr>
          <td>E5</td>
          <td>Payment provider timeout 變更</td>
          <td>06 + 04 + 09</td>
      </tr>
      <tr>
          <td>E6</td>
          <td>Webhook secret rotation</td>
          <td>07 + 04 + 08</td>
      </tr>
      <tr>
          <td>E7</td>
          <td>Flash-sale peak readiness</td>
          <td>09 + 02 + 03 + 06</td>
      </tr>
  </tbody>
</table>
<p>Episode 讓讀者看到「同一服務在不同壓力下需要不同模組」，同時保留單篇文章的原子性。</p>
<h3 id="第三步讓每章都回到同一條服務路徑">第三步：讓每章都回到同一條服務路徑</h3>
<p>每篇主章不需要都重述整個案例，但要能指出它在貫穿案例中的位置。這樣讀者可從任一章回到主線，也可按主線依序讀。</p>
<p>最小寫法：</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">本章在貫穿式 checkout 案例中處理 E3：訂單事件 consumer 失敗後，如何判斷投遞、處理與恢復語意。</span></span></code></pre></div><p>這句話把章節放回教學骨架，避免單章漂成孤立知識點。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="章節各自正確但整體難學">章節各自正確，但整體難學</h3>
<p>資料庫、快取、queue、觀測與事故章節都可以各自寫得正確。讀者看過它們如何在同一條服務路徑交接後，平行知識才會組成 production system 的整體模型。</p>
<h3 id="案例庫會停在素材層">案例庫會停在素材層</h3>
<p>大量 case 能支撐反向驗證，但 case 本身不會自動形成學習路線。貫穿式案例的責任是把素材庫轉成讀者可重播的主情境。</p>
<h3 id="vendor--migration-內容會太早成為主角">Vendor / migration 內容會太早成為主角</h3>
<p>讀者在還沒理解服務交接前讀 vendor 或 migration，容易把具體工具當成主線。貫穿式案例能先建立「問題如何跨模組流動」，再讓 vendor / migration 成為進階專題。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../source-library-ratio-supports-scenario-validation/">#98 素材庫比例要支撐主情境的反向驗證</a></td>
          <td>#98 說素材庫要支撐主情境；本卡定義服務教材的主情境應有一條貫穿式案例。</td>
      </tr>
      <tr>
          <td><a href="../case-citation-three-part-structure/">#120 案例引用三段式段落結構</a></td>
          <td>貫穿式案例在單段引用時仍要遵守概念定義 → case 引用 → 通用展開，不讓 case 取代概念。</td>
      </tr>
      <tr>
          <td><a href="../routing-layer-chapter-recognition/">#119 章節已有 routing skeleton 走補強段</a></td>
          <td>貫穿式案例是系列級 routing skeleton。後續擴章要補 episode 與路由，保留既有主線。</td>
      </tr>
      <tr>
          <td><a href="../teaching-goal-before-decision-frame/">#130 教材目標先於決策框架</a></td>
          <td>#130 定義教材目標，本卡提供讓目標落地的案例骨架。</td>
      </tr>
      <tr>
          <td><a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證</a></td>
          <td>讀者旅程回答「怎麼讀」，貫穿式案例回答「沿著什麼服務情境練」。</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>模組章節很多，但讀者不知道它們怎麼串成一個服務</td>
          <td>補貫穿式案例</td>
      </tr>
      <tr>
          <td>每篇文章都用不同業務情境，跨章記憶成本高</td>
          <td>收斂到 1 條主案例 + 少量變體</td>
      </tr>
      <tr>
          <td>案例庫豐富但主文章仍像概念清單</td>
          <td>把案例轉成可重播 episode</td>
      </tr>
      <tr>
          <td>Vendor / migration 內容比服務主線更顯眼</td>
          <td>用貫穿案例重新定義進階專題入口</td>
      </tr>
      <tr>
          <td>跨模組 link 多，但沒有共同 user journey</td>
          <td>補 episode map 與主線導讀</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：服務型教材要有貫穿式案例。能力分類讓作者整理內容，讀者旅程讓讀者知道怎麼讀，貫穿式案例讓讀者看到多個能力如何在同一個 production service 中交接與演進。</p>
]]></content:encoded></item><item><title>服務頁教材合約</title><link>https://tarrragon.github.io/blog/report/service-page-teaching-contract/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/service-page-teaching-contract/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>服務頁教材合約的責任是把「某個服務是什麼」寫成「讀者能學會什麼」。服務頁可以討論 PostgreSQL、SQLite、MongoDB、Redis、Kafka、Kubernetes、Okta 或 k6，但文章目標應是教讀者理解該服務承擔的系統責任、服務對象、操作語意、失敗訊號與替代邊界。&lt;/p>
&lt;p>服務頁教材合約把 vendor profile 升級成可教學的單篇文章。Vendor profile 描述功能、價格、適用場景與競品；服務頁教材還要讓讀者取得一個可遷移的心智模型，讀完後能把同一套判讀方式帶到相鄰服務。&lt;/p>
&lt;p>這份合約約束的是教學功能，不約束章節模板。Go 目錄提供的是討論細節、漸進教學、操作判讀與邊界意識的成熟度參照；Backend 服務頁要達到同等教學深度，但 SQLite、MongoDB、PostgreSQL、Redis、Kafka 或 Okta 的章節順序可以完全不同。&lt;/p>
&lt;h2 id="warp-分析摘要">WARP 分析摘要&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>Anchor&lt;/td>
 &lt;td>這次決策錨點從「服務清單是否完整」上移到「Backend 服務頁是否已達教材層級」。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Step 0&lt;/td>
 &lt;td>現有資料足以判斷：Backend vendor index 已完整，但服務頁正文成熟度不均；Go 目錄提供單篇教材成熟度參照。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Widen&lt;/td>
 &lt;td>選項有三種：統一章節模板、完全自由撰寫、定義教學功能合約。第三種能保護教學深度，也能保留服務差異。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reality Test&lt;/td>
 &lt;td>抽樣 Redis、Kafka、Kubernetes、Okta 接近成熟教材；PostgreSQL、SQLite、MongoDB、k6 需要依服務對象重設教學路線。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Prepare&lt;/td>
 &lt;td>若缺少合約就批量補正文，服務頁容易回到產品介紹、選型摘要或容量分析，教學完整度會持續漂移。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向驗證：章節統一會傷害服務頁。Go 目錄的價值是提供成熟教材的功能檢查，Backend 服務頁仍依自己的服務責任、讀者對象、案例壓力與操作比例安排標題。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>Backend 已完成總體教學設計、checkout episode map、vendor index audit 與各分類服務順序同步。下一步評估時，讀者提出更高標準：每個服務的探討內容與教學量級，應接近一篇成熟技術教材，讓選型摘要與 vendor backlog 成為教材材料。&lt;/p>
&lt;p>這個標準指向教學深度，而不是統一章節。SQLite 服務的讀者可能在意 embedded state、local-first、測試資料與低操作成本；MongoDB 服務的讀者可能在意 document shape、schema governance、index 與 transaction boundary。兩篇都要達到成熟教材深度，但學習路線應由服務對象決定。&lt;/p>
&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>服務存在於 &lt;code>vendors/_index.md&lt;/code>，有類型與撰寫批次&lt;/td>
 &lt;td>只證明有入口，尚未形成教材&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大綱完成&lt;/td>
 &lt;td>服務頁有定位、目標、操作、進階、排錯、案例與路由骨架&lt;/td>
 &lt;td>可以開寫，但仍需檢查段落深度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>教材完成&lt;/td>
 &lt;td>每段能教讀者判讀服務責任、操作訊號、替代邊界與失敗模式&lt;/td>
 &lt;td>可作為長期教材單篇交付&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>服務頁若只停在第一層，讀者會看到很多服務名稱；若能到第三層，讀者會理解服務能力如何在 production system 中承擔責任。&lt;/p>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>學習目標&lt;/td>
 &lt;td>可以是「本章目標」、開場學習成果、或讀法段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>核心概念&lt;/td>
 &lt;td>可以是服務定位、服務對象、資料形狀、流量形狀或控制面責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作形狀&lt;/td>
 &lt;td>可以是 CLI / API、schema 設計、工作流、平台操作或治理流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>判讀訊號&lt;/td>
 &lt;td>可以是 metrics、logs、query、事件、audit trail、cost signal 或人工流程訊號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>替代邊界&lt;/td>
 &lt;td>可以是同類服務比較、相鄰分類路由、或規模 / 組織能力分界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>案例回寫&lt;/td>
 &lt;td>可以是公開案例、反例、規模對照或 checkout episode&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>章節名稱要服務該服務的學習路線。SQLite 可以先談「正式狀態能否在單機成立」，MongoDB 可以先談「document shape 與 schema governance」，PostgreSQL 可以先談「SQL baseline 與 transaction」，這三篇不應被壓成同一套段落順序。&lt;/p>
&lt;h3 id="學習目標要先於服務介紹">學習目標要先於服務介紹&lt;/h3>
&lt;p>服務頁開頭要先說讀者讀完後能做什麼判斷。產品名稱與服務定位很重要，但學習目標決定文章是否是教材。&lt;/p>
&lt;p>成熟服務頁至少要回答：&lt;/p>
&lt;ul>
&lt;li>讀者能辨識這個服務承擔哪類 backend 責任。&lt;/li>
&lt;li>讀者能知道這個服務解哪種壓力，承擔哪種操作成本。&lt;/li>
&lt;li>讀者能用訊號判斷服務健康、退化或失配。&lt;/li>
&lt;li>讀者能知道何時改走相鄰服務。&lt;/li>
&lt;/ul>
&lt;h3 id="概念段要建立服務專屬心智模型">概念段要建立服務專屬心智模型&lt;/h3>
&lt;p>概念段的責任是建立讀者能帶走的模型。Redis 頁不只教 Redis command，還要教「可重建副本如何保護 origin」；Kafka 頁不只教 topic，還要教「event log、consumer progress 與 replay window」；Kubernetes 頁不只教 YAML，還要教「workload lifecycle、readiness、traffic 與 rollback contract」。&lt;/p>
&lt;p>服務專屬心智模型要貼近服務對象。SQLite 面向小型正式狀態、local data、edge 與測試資料；MongoDB 面向 document model、聚合查詢、索引與 schema governance；PostgreSQL 面向 SQL baseline、transaction、query boundary 與 schema evolution。三者同屬 database，教學路線仍可完全不同。&lt;/p>
&lt;h3 id="操作段要服務判讀">操作段要服務判讀&lt;/h3>
&lt;p>操作段的責任是讓讀者理解日常決策形狀。指令、設定、console 或 CLI 範例可以存在，但每個例子都要回到訊號、風險或下一步判斷。&lt;/p>
&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;/td>
 &lt;td>讓讀者快速判斷是否該考慮這個服務&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>日常操作形狀&lt;/td>
 &lt;td>說明服務平常怎麼被維護、監控與調整&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗快速判讀&lt;/td>
 &lt;td>說明異常發生時先看哪些訊號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evidence route&lt;/td>
 &lt;td>說明哪些結果要交給 observability / gate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="替代邊界要讓服務回到能力地圖">替代邊界要讓服務回到能力地圖&lt;/h3>
&lt;p>服務頁要寫「何時改走其他服務」。這段把服務放回能力地圖：正式狀態回 database、可重建副本回 cache、durable replay 回 queue、traffic lifecycle 回 deployment、release evidence 回 reliability、identity control 回 security、協作交接回 incident。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>服務頁教材合約的責任是把「某個服務是什麼」寫成「讀者能學會什麼」。服務頁可以討論 PostgreSQL、SQLite、MongoDB、Redis、Kafka、Kubernetes、Okta 或 k6，但文章目標應是教讀者理解該服務承擔的系統責任、服務對象、操作語意、失敗訊號與替代邊界。</p>
<p>服務頁教材合約把 vendor profile 升級成可教學的單篇文章。Vendor profile 描述功能、價格、適用場景與競品；服務頁教材還要讓讀者取得一個可遷移的心智模型，讀完後能把同一套判讀方式帶到相鄰服務。</p>
<p>這份合約約束的是教學功能，不約束章節模板。Go 目錄提供的是討論細節、漸進教學、操作判讀與邊界意識的成熟度參照；Backend 服務頁要達到同等教學深度，但 SQLite、MongoDB、PostgreSQL、Redis、Kafka 或 Okta 的章節順序可以完全不同。</p>
<h2 id="warp-分析摘要">WARP 分析摘要</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anchor</td>
          <td>這次決策錨點從「服務清單是否完整」上移到「Backend 服務頁是否已達教材層級」。</td>
      </tr>
      <tr>
          <td>Step 0</td>
          <td>現有資料足以判斷：Backend vendor index 已完整，但服務頁正文成熟度不均；Go 目錄提供單篇教材成熟度參照。</td>
      </tr>
      <tr>
          <td>Widen</td>
          <td>選項有三種：統一章節模板、完全自由撰寫、定義教學功能合約。第三種能保護教學深度，也能保留服務差異。</td>
      </tr>
      <tr>
          <td>Reality Test</td>
          <td>抽樣 Redis、Kafka、Kubernetes、Okta 接近成熟教材；PostgreSQL、SQLite、MongoDB、k6 需要依服務對象重設教學路線。</td>
      </tr>
      <tr>
          <td>Prepare</td>
          <td>若缺少合約就批量補正文，服務頁容易回到產品介紹、選型摘要或容量分析，教學完整度會持續漂移。</td>
      </tr>
  </tbody>
</table>
<p>反向驗證：章節統一會傷害服務頁。Go 目錄的價值是提供成熟教材的功能檢查，Backend 服務頁仍依自己的服務責任、讀者對象、案例壓力與操作比例安排標題。</p>
<h2 id="情境">情境</h2>
<p>Backend 已完成總體教學設計、checkout episode map、vendor index audit 與各分類服務順序同步。下一步評估時，讀者提出更高標準：每個服務的探討內容與教學量級，應接近一篇成熟技術教材，讓選型摘要與 vendor backlog 成為教材材料。</p>
<p>這個標準指向教學深度，而不是統一章節。SQLite 服務的讀者可能在意 embedded state、local-first、測試資料與低操作成本；MongoDB 服務的讀者可能在意 document shape、schema governance、index 與 transaction boundary。兩篇都要達到成熟教材深度，但學習路線應由服務對象決定。</p>
<p>這個提醒揭露了三種不同完成度：</p>
<table>
  <thead>
      <tr>
          <th>完成度</th>
          <th>訊號</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引完成</td>
          <td>服務存在於 <code>vendors/_index.md</code>，有類型與撰寫批次</td>
          <td>只證明有入口，尚未形成教材</td>
      </tr>
      <tr>
          <td>大綱完成</td>
          <td>服務頁有定位、目標、操作、進階、排錯、案例與路由骨架</td>
          <td>可以開寫，但仍需檢查段落深度</td>
      </tr>
      <tr>
          <td>教材完成</td>
          <td>每段能教讀者判讀服務責任、操作訊號、替代邊界與失敗模式</td>
          <td>可作為長期教材單篇交付</td>
      </tr>
  </tbody>
</table>
<p>服務頁若只停在第一層，讀者會看到很多服務名稱；若能到第三層，讀者會理解服務能力如何在 production system 中承擔責任。</p>
<h2 id="服務頁教材功能合約">服務頁教材功能合約</h2>
<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>可以是 CLI / API、schema 設計、工作流、平台操作或治理流程</td>
      </tr>
      <tr>
          <td>判讀訊號</td>
          <td>可以是 metrics、logs、query、事件、audit trail、cost signal 或人工流程訊號</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>可以是同類服務比較、相鄰分類路由、或規模 / 組織能力分界</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>可以是公開案例、反例、規模對照或 checkout episode</td>
      </tr>
  </tbody>
</table>
<p>章節名稱要服務該服務的學習路線。SQLite 可以先談「正式狀態能否在單機成立」，MongoDB 可以先談「document shape 與 schema governance」，PostgreSQL 可以先談「SQL baseline 與 transaction」，這三篇不應被壓成同一套段落順序。</p>
<h3 id="學習目標要先於服務介紹">學習目標要先於服務介紹</h3>
<p>服務頁開頭要先說讀者讀完後能做什麼判斷。產品名稱與服務定位很重要，但學習目標決定文章是否是教材。</p>
<p>成熟服務頁至少要回答：</p>
<ul>
<li>讀者能辨識這個服務承擔哪類 backend 責任。</li>
<li>讀者能知道這個服務解哪種壓力，承擔哪種操作成本。</li>
<li>讀者能用訊號判斷服務健康、退化或失配。</li>
<li>讀者能知道何時改走相鄰服務。</li>
</ul>
<h3 id="概念段要建立服務專屬心智模型">概念段要建立服務專屬心智模型</h3>
<p>概念段的責任是建立讀者能帶走的模型。Redis 頁不只教 Redis command，還要教「可重建副本如何保護 origin」；Kafka 頁不只教 topic，還要教「event log、consumer progress 與 replay window」；Kubernetes 頁不只教 YAML，還要教「workload lifecycle、readiness、traffic 與 rollback contract」。</p>
<p>服務專屬心智模型要貼近服務對象。SQLite 面向小型正式狀態、local data、edge 與測試資料；MongoDB 面向 document model、聚合查詢、索引與 schema governance；PostgreSQL 面向 SQL baseline、transaction、query boundary 與 schema evolution。三者同屬 database，教學路線仍可完全不同。</p>
<h3 id="操作段要服務判讀">操作段要服務判讀</h3>
<p>操作段的責任是讓讀者理解日常決策形狀。指令、設定、console 或 CLI 範例可以存在，但每個例子都要回到訊號、風險或下一步判斷。</p>
<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>Evidence route</td>
          <td>說明哪些結果要交給 observability / gate</td>
      </tr>
  </tbody>
</table>
<h3 id="替代邊界要讓服務回到能力地圖">替代邊界要讓服務回到能力地圖</h3>
<p>服務頁要寫「何時改走其他服務」。這段把服務放回能力地圖：正式狀態回 database、可重建副本回 cache、durable replay 回 queue、traffic lifecycle 回 deployment、release evidence 回 reliability、identity control 回 security、協作交接回 incident。</p>
<h3 id="案例回寫要提供情境壓力">案例回寫要提供情境壓力</h3>
<p>案例回寫的責任是讓服務頁有真實壓力來源。案例要提供流量形狀、資料形狀、組織能力、失敗代價或回退路徑；案例只停在「某公司使用 X」時，對教材幫助有限。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="服務頁會變成產品百科">服務頁會變成產品百科</h3>
<p>產品百科容易累積功能、版本、價格與競品資訊，讀者可以查到很多答案，但仍難以建立系統責任模型。服務頁教材合約要求每個資訊都回到教學責任。</p>
<h3 id="統一模板會抹平服務對象">統一模板會抹平服務對象</h3>
<p>統一模板會把同分類服務壓成相同讀法，讓服務對象消失。SQLite、MongoDB、DynamoDB、PostgreSQL 都在 database 分類，但它們回答的讀者問題不同；用同一套章節順序處理，會讓 embedded state、document model、access pattern 與 SQL transaction 的差異變成表格欄位，而不是學習路線。</p>
<h3 id="內容會被選型語氣帶走">內容會被選型語氣帶走</h3>
<p>選型語氣會問「什麼情境選 X」，但教材還要問「X 在系統中承擔什麼責任」。只寫選型，讀者能做局部選擇；補上教材合約，讀者能理解整個 backend 能力地圖。</p>
<h3 id="大量服務頁會產生完成錯覺">大量服務頁會產生完成錯覺</h3>
<p>服務頁數量增加會帶來覆蓋感，但覆蓋不等於完成。125 個服務頁如果只有入口與摘要，仍是素材庫；每篇具備學習目標、操作判讀與案例回寫後，才是教材。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../teaching-goal-before-decision-frame/">#130 教材目標先於決策框架</a></td>
          <td>#130 定義教材的上位目標；本卡把上位目標落到「單篇服務頁」的教學功能合約。</td>
      </tr>
      <tr>
          <td><a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證</a></td>
          <td>#131 用讀者旅程檢查系列完整性；本卡用單篇教材合約檢查服務頁是否能獨立教會一個服務能力。</td>
      </tr>
      <tr>
          <td><a href="../throughline-case-as-teaching-spine/">#132 貫穿式案例是服務教材的教學骨架</a></td>
          <td>#132 提供跨模組案例主線；本卡要求每篇服務頁能回寫至少一段 case route 或明確標示案例缺口。</td>
      </tr>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></td>
          <td>服務頁教材合約也要檢查 title、description、heading、index entry 與下一步路由，避免正文與入口語意不一致。</td>
      </tr>
      <tr>
          <td><a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性</a></td>
          <td>服務頁 review 要同時看教材目標、服務責任、操作訊號、案例回寫、metadata surface 與跨模組路由。</td>
      </tr>
      <tr>
          <td><a href="../cadence-homogenization-in-batch-writing/">#122 Cadence 同質化是模板的隱形維度</a></td>
          <td>本卡補服務頁的結構層防護：教學功能要完整，章節順序要依服務對象與責任形狀調整，避免批量服務頁同質化。</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務頁只有定位、適用場景與競品比較</td>
          <td>補本章目標、操作形狀、排錯判讀與案例回寫</td>
      </tr>
      <tr>
          <td>服務頁內容很豐富，但缺少清楚學習路線</td>
          <td>依服務對象重排 heading</td>
      </tr>
      <tr>
          <td>排錯段只有常見錯誤列表</td>
          <td>改成「訊號 → 判讀 → 下一步路由」</td>
      </tr>
      <tr>
          <td>案例段只列公司名稱</td>
          <td>補案例提供的壓力、失敗代價或回退條件</td>
      </tr>
      <tr>
          <td>服務頁讀完仍不知道下一步讀哪裡</td>
          <td>補上游概念、平行服務、下游 artifact 與 case route</td>
      </tr>
      <tr>
          <td>批量服務頁都套同一標題但失去分類語言</td>
          <td>回到分類責任調整段落順序</td>
      </tr>
      <tr>
          <td>同分類服務頁看起來像同一篇文章換名詞</td>
          <td>重新辨識每個服務的讀者對象、服務責任與操作語境</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：服務頁完成的標準是讀者能透過單篇文章學會一個服務能力。成熟服務頁要同時提供學習目標、概念模型、操作判讀、替代邊界、案例回寫與下一步路由。</p>
]]></content:encoded></item><item><title>Sibling Coverage Asymmetry Blindspot：Priority 評估漏掉的「對稱性維度」</title><link>https://tarrragon.github.io/blog/report/sibling-coverage-asymmetry-blindspot-in-priority/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/sibling-coverage-asymmetry-blindspot-in-priority/</guid><description>&lt;h2 id="核心priority-評估的-sibling-對稱性盲點">核心：Priority 評估的 sibling 對稱性盲點&lt;/h2>
&lt;p>當批量 A 跟批量 B 是 &lt;em>sibling&lt;/em>（同類 vendor / 同類角色 / 應有對等 coverage）、但 A 後寫卻超過 B、心智模型容易 collapse 到「A 是 reference template / B 是 baseline」的角色分配、忽略 &lt;em>B 才該 ≥ A coverage 的對稱性 priority&lt;/em>。Priority 列表往往跳過 B、列其他「新領域擴張」選項。&lt;/p>
&lt;p>問題不在 &lt;em>推某個 vendor&lt;/em>、在 &lt;em>priority 評估維度漏掉 sibling symmetry&lt;/em>。&lt;/p>
&lt;h2 id="casemysql-18-篇-vs-pg-11-篇後的-priority-列表">Case：MySQL 18 篇 vs PG 11 篇後的 priority 列表&lt;/h2>
&lt;p>時間線：&lt;/p>
&lt;ol>
&lt;li>PG 11 篇先寫完（autovacuum-tuning / declarative-partitioning / patroni-ha / pgbouncer-config / pitr-wal-archiving / logical-replication-debezium + 5 migration playbook）&lt;/li>
&lt;li>MySQL 從 0 開始、user 要求「第一個示範服務、儘量都寫」、寫到 17 篇 deep article + migration playbook + 既有 migrate-to-postgresql = 18 篇 / 5715 行&lt;/li>
&lt;li>推薦下一步 priority 時、列「DynamoDB / Aurora / SQLite / MongoDB / CockroachDB / Spanner / Cosmos DB」、PG &lt;strong>不在列表&lt;/strong>&lt;/li>
&lt;li>User 問：「為什麼這裡列的選項沒有 PG？我們做完了嗎？」&lt;/li>
&lt;/ol>
&lt;p>實際盤點：&lt;/p>
&lt;ul>
&lt;li>PG 11 篇 vs MySQL 18 篇、PG 缺 &lt;strong>7 個 MySQL sibling deep article&lt;/strong>（replication-topology / online-schema-change-tools / query-optimization / lock-contention / vitess-sharding 對應 Citus / group-replication 對應 BDR / modern-sql-features 反向視角）&lt;/li>
&lt;li>PG 還缺 &lt;strong>4 個 PG-only 議題&lt;/strong>（JSONB deep dive / Extension ecosystem / Full-text search / Replication slot management）&lt;/li>
&lt;/ul>
&lt;p>User 直覺 catch 到 &lt;em>coverage asymmetry&lt;/em>、但我 priority 列表沒提供這個視角。&lt;/p>
&lt;h2 id="機制為什麼會忽略">機制：為什麼會忽略&lt;/h2>
&lt;p>至少 5 個 priority bias 共同貢獻：&lt;/p>
&lt;h3 id="1-先存在就-mature隱性假設">1. 「先存在就 mature」隱性假設&lt;/h3>
&lt;p>PG 11 篇先存在 → 直覺映射「PG 已 mature」。沒做 &lt;em>cross-sectional 對比&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>PG 11 篇 vs MySQL 18 篇、絕對量比較&lt;/li>
&lt;li>議題覆蓋對應：MySQL 有哪些 deep article、PG 對應的是否都有&lt;/li>
&lt;/ul>
&lt;p>「11 篇」這個絕對數字 &lt;em>看起來合理&lt;/em>、但跟 MySQL 18 篇對比後 &lt;em>結構性不足&lt;/em>。心智模型把「合理」當成「mature」、跳過了相對性 audit。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心priority-評估的-sibling-對稱性盲點">核心：Priority 評估的 sibling 對稱性盲點</h2>
<p>當批量 A 跟批量 B 是 <em>sibling</em>（同類 vendor / 同類角色 / 應有對等 coverage）、但 A 後寫卻超過 B、心智模型容易 collapse 到「A 是 reference template / B 是 baseline」的角色分配、忽略 <em>B 才該 ≥ A coverage 的對稱性 priority</em>。Priority 列表往往跳過 B、列其他「新領域擴張」選項。</p>
<p>問題不在 <em>推某個 vendor</em>、在 <em>priority 評估維度漏掉 sibling symmetry</em>。</p>
<h2 id="casemysql-18-篇-vs-pg-11-篇後的-priority-列表">Case：MySQL 18 篇 vs PG 11 篇後的 priority 列表</h2>
<p>時間線：</p>
<ol>
<li>PG 11 篇先寫完（autovacuum-tuning / declarative-partitioning / patroni-ha / pgbouncer-config / pitr-wal-archiving / logical-replication-debezium + 5 migration playbook）</li>
<li>MySQL 從 0 開始、user 要求「第一個示範服務、儘量都寫」、寫到 17 篇 deep article + migration playbook + 既有 migrate-to-postgresql = 18 篇 / 5715 行</li>
<li>推薦下一步 priority 時、列「DynamoDB / Aurora / SQLite / MongoDB / CockroachDB / Spanner / Cosmos DB」、PG <strong>不在列表</strong></li>
<li>User 問：「為什麼這裡列的選項沒有 PG？我們做完了嗎？」</li>
</ol>
<p>實際盤點：</p>
<ul>
<li>PG 11 篇 vs MySQL 18 篇、PG 缺 <strong>7 個 MySQL sibling deep article</strong>（replication-topology / online-schema-change-tools / query-optimization / lock-contention / vitess-sharding 對應 Citus / group-replication 對應 BDR / modern-sql-features 反向視角）</li>
<li>PG 還缺 <strong>4 個 PG-only 議題</strong>（JSONB deep dive / Extension ecosystem / Full-text search / Replication slot management）</li>
</ul>
<p>User 直覺 catch 到 <em>coverage asymmetry</em>、但我 priority 列表沒提供這個視角。</p>
<h2 id="機制為什麼會忽略">機制：為什麼會忽略</h2>
<p>至少 5 個 priority bias 共同貢獻：</p>
<h3 id="1-先存在就-mature隱性假設">1. 「先存在就 mature」隱性假設</h3>
<p>PG 11 篇先存在 → 直覺映射「PG 已 mature」。沒做 <em>cross-sectional 對比</em>：</p>
<ul>
<li>PG 11 篇 vs MySQL 18 篇、絕對量比較</li>
<li>議題覆蓋對應：MySQL 有哪些 deep article、PG 對應的是否都有</li>
</ul>
<p>「11 篇」這個絕對數字 <em>看起來合理</em>、但跟 MySQL 18 篇對比後 <em>結構性不足</em>。心智模型把「合理」當成「mature」、跳過了相對性 audit。</p>
<h3 id="2-新領域擴張優於既有領域對齊的-progress-bias">2. 「新領域擴張」優於「既有領域對齊」的 progress bias</h3>
<p>Priority 列表時、DynamoDB / Aurora / SQLite 等 vendor <em>看起來進度感強</em> — 從 0 推到 N、新領域擴張。PG 補齊看起來 <em>重複勞動</em> — 從 11 推到 18、改善舊領域。</p>
<p>實際上：</p>
<ul>
<li>新領域擴張 <em>增加 surface area</em>、但不改善既有結構</li>
<li>既有領域對齊 <em>修補 baseline</em>、是 reference template 成立的前提</li>
</ul>
<p>當 baseline 跟 reference template 不對稱時、後者作為 <em>示範服務</em> 的價值打折扣 — 「MySQL 怎麼寫 vendor article」沒法 fully 套到 PG、因為 PG 本身不對稱。</p>
<h3 id="3-priority-評估維度漏-sibling-symmetry">3. Priority 評估維度漏 sibling symmetry</h3>
<p>我用的 priority 評估維度：</p>
<ul>
<li>T1 vs T2 vendor 分類</li>
<li>領域重要度</li>
<li>已有量</li>
<li>新領域 vs 既有領域</li>
</ul>
<p><strong>漏掉的維度</strong>：</p>
<ul>
<li>Sibling vendor 對稱性（A 跟 B 同類、A 寫完後 B coverage 是否對齊）</li>
<li>Reference template 跟 baseline 的關係（後寫的 reference template 應 ≤ baseline）</li>
</ul>
<p>「Sibling 對稱性」這個維度不在預設 priority 評估清單、就被自動忽略。</p>
<h3 id="4-reference-template-vs-baseline-角色混淆">4. Reference template vs Baseline 角色混淆</h3>
<p>寫 vendor article 時、<em>哪個是 baseline、哪個是 reference template</em> 的心智模型可能反轉：</p>
<ul>
<li>直覺：「先寫的 = baseline、後寫的 = reference / extension」</li>
<li>真實：「baseline 應 ≥ reference template coverage、不該倒過來」</li>
</ul>
<p>MySQL 18 篇是 <em>user-driven 要求</em> — user 明說「第一個示範服務、儘量都寫」。所以 MySQL 寫得多不是錯。但 <em>PG 沒對齊到同水準</em> 才是漏掉的紀律。</p>
<p>當 MySQL 寫到 reference template 規模、PG 還在 11 篇、心智模型容易 collapse 到「MySQL 是新 baseline、PG 是 legacy partial」、其實是 <em>baseline 應該升級到 reference template 水準</em>。</p>
<h3 id="5-sequential-vs-cross-sectional-coverage-評估">5. Sequential vs cross-sectional coverage 評估</h3>
<p>寫作過程是 sequential —寫 MySQL 17 篇是一段時間、寫完看 git diff stat 確認進度、然後 priority 下一步。<strong>Coverage 評估是 point-in-time 的</strong>：</p>
<ul>
<li>Point-in-time（sequential）：「我這 batch 寫了多少」</li>
<li>Cross-sectional（symmetric）：「我寫的這個跟 sibling 是否對齊」</li>
</ul>
<p>寫 MySQL 第 17 篇時 self-cross-check：「PG 對應有沒有？」是 cross-sectional 行為、不是預設行為。</p>
<p>Priority 列表階段沒回頭跑 cross-sectional audit、就把 PG 排除。</p>
<h2 id="修法">修法</h2>
<h3 id="1-priority-candidate-list-必須跑-sibling-symmetry-audit">1. Priority candidate list 必須跑 sibling symmetry audit</h3>
<p>提 priority 列表時、強制 cross-check：</p>
<ul>
<li>列出該批量影響的 <em>sibling vendor / sibling role</em></li>
<li>對比每個 sibling 的 coverage（篇數 + 議題覆蓋 mapping）</li>
<li>若有 asymmetry、把「補齊 sibling」加進 priority 列表 <em>跟新領域並列</em></li>
</ul>
<h3 id="2-vendors_index內容覆蓋進度表加對稱性視角">2. Vendors/_index「內容覆蓋進度」表加對稱性視角</h3>
<p>當前內容覆蓋進度只列「已寫 / 未寫」、不列 <em>sibling 之間相對進度</em>。改善：</p>
<ul>
<li>加 <em>「跟 sibling 對應」欄</em>：每個 article 標 sibling vendor 是否有對應</li>
<li>加 <em>總計篇數 + sibling 對比</em> 欄：直觀看到 asymmetry</li>
</ul>
<h3 id="3-先-mature-baseline再擴張紀律">3. 「先 mature baseline、再擴張」紀律</h3>
<p>寫 vendor batch 時、紀律：</p>
<ul>
<li>確認 <em>baseline vendor 對齊到 reference template 水準</em>、再推下一個 vendor</li>
<li>例外：user 明確要求先擴張某 vendor 時、加註 <em>baseline 待對齊</em> 為 known limitation</li>
</ul>
<h3 id="4-audit-dimension-list-加-coverage-symmetry">4. Audit dimension list 加 <em>Coverage symmetry</em></h3>
<p>跟 <a href="../data-topology-as-audit-dimension/">Data Topology as Audit Dimension</a> 同型 —audit 維度可擴張。把 <em>sibling coverage symmetry</em> 加進 priority audit 維度：</p>
<ul>
<li>既有維度：T1 / 領域 / 已有量 / 新 vs 既有</li>
<li>新增維度：<strong>sibling 對稱性</strong>（A 跟 B 同類時、coverage 對齊度）</li>
</ul>
<h2 id="跟既有原則的關係">跟既有原則的關係</h2>
<ul>
<li><a href="../data-topology-as-audit-dimension/">Data Topology as Audit Dimension</a>：本卡是 <em>priority 評估維度漏一個</em>、同型但不同 axis</li>
<li><a href="../collapse-is-implicit-default/">Collapse is Implicit Default</a>：priority 評估 collapse 到「新領域擴張」維度、是其變體</li>
<li><a href="../multi-pass-review-frame-granularity-blindspot/">Multi-Pass Review Frame Granularity Blindspot</a>：multi-pass review 漏 catch 的同型、但本卡是 <em>priority assessment 漏 catch</em>、不是 <em>review 漏 catch</em></li>
</ul>
<h2 id="反向驗證">反向驗證</h2>
<p>不該誤用本卡：</p>
<ul>
<li><em>Sibling vendor 對稱性</em> 不等於 <em>每個 vendor 都該寫到同篇數</em>。MySQL 18 篇對 PG 合理（兩大 SQL OLTP baseline），但 SQLite / DynamoDB / Spanner 各 18 篇不合理（領域窄 / niche audience）</li>
<li>對稱性 audit 是 <em>對 baseline / reference template 雙方適用</em>、不是擴張到所有 sibling</li>
<li>真正 niche vendor（如 Spanner / Cosmos DB 對小團隊）可以 <em>明確 backlog 標記 minimum coverage</em>、不必對齊 baseline</li>
</ul>
<h2 id="觸發再評估">觸發再評估</h2>
<p>未來累積到以下情境、本卡應重新 review：</p>
<ul>
<li>寫第二個 baseline pair（02 cache Redis vs Memcached / 03 queue Kafka vs NATS 等）時、是否同樣踩 asymmetry blindspot</li>
<li>多 reviewer audit 是否能 catch coverage asymmetry（4-reviewer 沒設計這軸、之後 batch 可加 reviewer E <em>coverage symmetry</em>）</li>
<li>Sibling 對稱性 audit 進工具化（vendors/_index 自動產 asymmetry warning）後是否解決</li>
</ul>
]]></content:encoded></item><item><title>Sibling Vendor Cross-Link 雙向性 Audit：寫 Vendor Batch 結束必跑</title><link>https://tarrragon.github.io/blog/report/sibling-vendor-cross-link-bidirectionality-audit/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/sibling-vendor-cross-link-bidirectionality-audit/</guid><description>&lt;h2 id="核心sibling-vendor-batch-容易單向-cross-link">核心：Sibling vendor batch 容易單向 cross-link&lt;/h2>
&lt;p>當寫 sibling vendor batch（A 跟 B 是同類角色的 vendor）、cross-link 容易單向：&lt;/p>
&lt;ul>
&lt;li>A 是後寫 batch、提 B 多次（「跟 PG sibling 對比」「PG 的 X 行為跟 MySQL 不同」）&lt;/li>
&lt;li>B 是先寫 batch、預設沒提 A（寫的時候 A 還不存在）&lt;/li>
&lt;li>結果：A → B 有 9 條 link、B → A 有 0 條 link&lt;/li>
&lt;/ul>
&lt;p>讀者從 B 進入、看不到 A 的存在；只有從 A 進入才知道兩者並列。&lt;/p>
&lt;p>問題不在 &lt;em>單向 link 本身錯&lt;/em>、在 &lt;em>vendor batch 結束沒跑 bidirectional audit&lt;/em>、就以為「cross-link 已建立」。&lt;/p>
&lt;h2 id="casemysql--postgresql-cross-link-asymmetry">Case：MySQL ↔ PostgreSQL cross-link asymmetry&lt;/h2>
&lt;p>4-reviewer audit（Reviewer B）finding：&lt;/p>
&lt;ul>
&lt;li>MySQL 18 篇對 PG sibling 的 cross-link：9 條（vs PostgreSQL 對比段 / 連到 PG vendor page / 連到 PG sibling article）&lt;/li>
&lt;li>PG 11 篇對 MySQL 的 cross-link：0 條&lt;/li>
&lt;/ul>
&lt;p>讀者站 PG &lt;code>pgbouncer-config&lt;/code> 不會跳到 MySQL &lt;code>proxysql-config&lt;/code>；站 MySQL &lt;code>proxysql-config&lt;/code> 直接看到「跟 PG pgBouncer 對比」段。Navigation asymmetric。&lt;/p>
&lt;h2 id="機制為什麼會單向">機制：為什麼會單向&lt;/h2>
&lt;h3 id="1-寫第二個-batch-時-reference-第一個-batch-是自然行為">1. 寫第二個 batch 時 reference 第一個 batch 是自然行為&lt;/h3>
&lt;p>寫 MySQL &lt;code>replication-topology&lt;/code> 時、PG &lt;code>patroni-ha&lt;/code> 已存在、自然連去做對比。寫 PG &lt;code>patroni-ha&lt;/code> 時、MySQL &lt;code>replication-topology&lt;/code> 還不存在、不可能 link。&lt;/p>
&lt;p>這是 &lt;em>sequential 寫作的時間性結構性&lt;/em>、不是疏忽。&lt;/p>
&lt;h3 id="2-bidirectional-link-audit-不在預設寫作流程">2. Bidirectional link audit 不在預設寫作流程&lt;/h3>
&lt;p>寫完 batch B 後、預設 audit：&lt;/p>
&lt;ul>
&lt;li>lint / cards&lt;/li>
&lt;li>emoji / 裸 URL&lt;/li>
&lt;li>跨檔一致性&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>沒有&lt;/strong> &lt;em>向上回補 sibling A 的 cross-link&lt;/em> 這一步。&lt;/p>
&lt;h3 id="3-sibling-a-寫好後不會自動-trigger-a-的更新">3. Sibling A 寫好後、不會自動 trigger A 的更新&lt;/h3>
&lt;p>寫 vendor batch B 完成時、A 的內容不變、沒人 trigger「現在 sibling B 存在了、A 應該加 cross-link 回 B」。&lt;/p>
&lt;h2 id="修法bidirectional-cross-link-audit">修法：Bidirectional cross-link audit&lt;/h2>
&lt;h3 id="audit-步驟">Audit 步驟&lt;/h3>
&lt;p>寫完 vendor batch B（B 跟 sibling A 存在對應）後、跑：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. Count A → B link&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">rg -c &lt;span class="s2">&amp;#34;\]\(/path/to/B/&amp;#34;&lt;/span> content/path/to/A/*.md
&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 class="c1"># 2. Count B → A link&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">rg -c &lt;span class="s2">&amp;#34;\]\(/path/to/A/&amp;#34;&lt;/span> content/path/to/B/*.md
&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 class="c1"># 3. 對比&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 若 A→B 顯著少於 B→A、補 A 端 cross-link&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="補-a-端-cross-link-的位置">補 A 端 cross-link 的位置&lt;/h3>
&lt;p>每個 A article 應該在：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心sibling-vendor-batch-容易單向-cross-link">核心：Sibling vendor batch 容易單向 cross-link</h2>
<p>當寫 sibling vendor batch（A 跟 B 是同類角色的 vendor）、cross-link 容易單向：</p>
<ul>
<li>A 是後寫 batch、提 B 多次（「跟 PG sibling 對比」「PG 的 X 行為跟 MySQL 不同」）</li>
<li>B 是先寫 batch、預設沒提 A（寫的時候 A 還不存在）</li>
<li>結果：A → B 有 9 條 link、B → A 有 0 條 link</li>
</ul>
<p>讀者從 B 進入、看不到 A 的存在；只有從 A 進入才知道兩者並列。</p>
<p>問題不在 <em>單向 link 本身錯</em>、在 <em>vendor batch 結束沒跑 bidirectional audit</em>、就以為「cross-link 已建立」。</p>
<h2 id="casemysql--postgresql-cross-link-asymmetry">Case：MySQL ↔ PostgreSQL cross-link asymmetry</h2>
<p>4-reviewer audit（Reviewer B）finding：</p>
<ul>
<li>MySQL 18 篇對 PG sibling 的 cross-link：9 條（vs PostgreSQL 對比段 / 連到 PG vendor page / 連到 PG sibling article）</li>
<li>PG 11 篇對 MySQL 的 cross-link：0 條</li>
</ul>
<p>讀者站 PG <code>pgbouncer-config</code> 不會跳到 MySQL <code>proxysql-config</code>；站 MySQL <code>proxysql-config</code> 直接看到「跟 PG pgBouncer 對比」段。Navigation asymmetric。</p>
<h2 id="機制為什麼會單向">機制：為什麼會單向</h2>
<h3 id="1-寫第二個-batch-時-reference-第一個-batch-是自然行為">1. 寫第二個 batch 時 reference 第一個 batch 是自然行為</h3>
<p>寫 MySQL <code>replication-topology</code> 時、PG <code>patroni-ha</code> 已存在、自然連去做對比。寫 PG <code>patroni-ha</code> 時、MySQL <code>replication-topology</code> 還不存在、不可能 link。</p>
<p>這是 <em>sequential 寫作的時間性結構性</em>、不是疏忽。</p>
<h3 id="2-bidirectional-link-audit-不在預設寫作流程">2. Bidirectional link audit 不在預設寫作流程</h3>
<p>寫完 batch B 後、預設 audit：</p>
<ul>
<li>lint / cards</li>
<li>emoji / 裸 URL</li>
<li>跨檔一致性</li>
</ul>
<p><strong>沒有</strong> <em>向上回補 sibling A 的 cross-link</em> 這一步。</p>
<h3 id="3-sibling-a-寫好後不會自動-trigger-a-的更新">3. Sibling A 寫好後、不會自動 trigger A 的更新</h3>
<p>寫 vendor batch B 完成時、A 的內容不變、沒人 trigger「現在 sibling B 存在了、A 應該加 cross-link 回 B」。</p>
<h2 id="修法bidirectional-cross-link-audit">修法：Bidirectional cross-link audit</h2>
<h3 id="audit-步驟">Audit 步驟</h3>
<p>寫完 vendor batch B（B 跟 sibling A 存在對應）後、跑：</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. Count A → B link</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rg -c <span class="s2">&#34;\]\(/path/to/B/&#34;</span> content/path/to/A/*.md
</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="c1"># 2. Count B → A link</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">rg -c <span class="s2">&#34;\]\(/path/to/A/&#34;</span> content/path/to/B/*.md
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 3. 對比</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># 若 A→B 顯著少於 B→A、補 A 端 cross-link</span></span></span></code></pre></div><h3 id="補-a-端-cross-link-的位置">補 A 端 cross-link 的位置</h3>
<p>每個 A article 應該在：</p>
<ol>
<li><strong>「相關連結」段</strong> — 列對應 sibling B article</li>
<li><strong>「跟其他 vendor 的取捨」段</strong>（若有） — 提到 sibling B 的對應</li>
<li><strong>「下一步路由」 / 「替代路徑」段</strong> — 列 B 作為 alternative</li>
</ol>
<h3 id="audit-cadence">Audit cadence</h3>
<ul>
<li>每個 sibling vendor batch 寫完、跑一次 bidirectional audit</li>
<li>不只寫完 <em>第二個</em> batch、寫完 <em>第三 / 第四個</em> 也跑（A↔B↔C 三方對稱）</li>
<li>vendors/_index 內容覆蓋進度表加 <code>link_density</code> 欄、揭露 asymmetry</li>
</ul>
<h2 id="跟既有原則的關係">跟既有原則的關係</h2>
<ul>
<li><a href="../sibling-coverage-asymmetry-blindspot-in-priority/">Sibling Coverage Asymmetry Blindspot in Priority</a>：本卡是 cross-link asymmetry、那卡是 coverage asymmetry、同型但不同 axis</li>
<li><a href="../cards-as-living-system-iteration/">Cards as Living System Iteration</a>：cross-link 維護是 living system 部分、不是 one-shot</li>
</ul>
<h2 id="反向驗證">反向驗證</h2>
<p>不該誤用：</p>
<ul>
<li><em>Sibling vendor</em> 限同類角色（PG / MySQL 都 SQL baseline）、不是任意兩個 vendor。MySQL 沒必要 link Spanner（不同類）</li>
<li>雙向不等於 <em>對稱數量</em> — A 18 篇可能有 9 條 link、B 11 篇有 6 條 link 是合理（不是 9 對 9）</li>
<li>Migration playbook 結構性單向（A → B 是遷移、不是 B → A）— 對 migration playbook 是 <em>單向結構</em>、不適用本 audit</li>
</ul>
]]></content:encoded></item><item><title>Vendor Feature 時間敏感性：Claim Verification 必跑、寫作日期必標</title><link>https://tarrragon.github.io/blog/report/vendor-feature-time-sensitivity-claim-verification/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/vendor-feature-time-sensitivity-claim-verification/</guid><description>&lt;h2 id="核心vendor-feature-limitation-claim-有時間敏感性">核心：Vendor feature limitation claim 有時間敏感性&lt;/h2>
&lt;p>寫 vendor article 時、常見以下 claim 形態：&lt;/p>
&lt;ul>
&lt;li>「Vendor X 不支援 Y」&lt;/li>
&lt;li>「Vendor X 最多 Z」&lt;/li>
&lt;li>「Vendor X 預設 W」&lt;/li>
&lt;/ul>
&lt;p>這些 claim 在寫作那刻是真的、但 vendor 持續演進。寫作後 &lt;em>N 個月&lt;/em> — 6 個月、12 個月、24 個月 — claim 可能反轉、整段 audit 邏輯 invalidates。&lt;/p>
&lt;p>問題不只是 &lt;em>claim 過時&lt;/em>、是 &lt;em>基於 claim 的整段流程被推翻&lt;/em>。Migration playbook Phase 1 audit 如果以「Vendor 不支援 X」為前提、X 後來變支援、Phase 1 整段重寫。&lt;/p>
&lt;h2 id="caseplanetscale-fk-claim-反轉">Case：PlanetScale FK claim 反轉&lt;/h2>
&lt;p>寫 migrate-to-planetscale.md 跟 migrate-vitess-to-planetscale.md 時：&lt;/p>
&lt;ul>
&lt;li>Claim：「PlanetScale 不支援 Foreign Key（Vitess 限制）」&lt;/li>
&lt;li>基於此 claim：Phase 1 audit 整段「FK audit + 全 drop FK + application enforcement 改寫」&lt;/li>
&lt;li>Phase 1 是 weeks-months 工作量、第一個 phase&lt;/li>
&lt;/ul>
&lt;p>實際狀態（4-reviewer C audit catch）：&lt;/p>
&lt;ul>
&lt;li>Vitess 18（2023 末）加 FK 支援&lt;/li>
&lt;li>PlanetScale 2024 起在合適 plan 內可啟用 FK&lt;/li>
&lt;li>「不支援」是 2022 年的事實、寫作時已過時&lt;/li>
&lt;/ul>
&lt;p>修法：整段 Phase 1 audit 從「FK audit + drop」改寫成「FK 行為驗證 + cross-shard cascade 處理」。&lt;/p>
&lt;p>這不是 &lt;em>微調文字&lt;/em>、是 &lt;em>整段 framing 重做&lt;/em>。&lt;/p>
&lt;h2 id="機制為什麼會發生">機制：為什麼會發生&lt;/h2>
&lt;h3 id="1-llm-training-cutoff-vs-vendor-changelog-速度差">1. LLM training cutoff vs vendor changelog 速度差&lt;/h3>
&lt;p>LLM training data 有 cutoff date（通常滯後 12-18 個月）。Vendor major feature release 在 cutoff 後、LLM 不知道。&lt;/p>
&lt;p>寫 vendor article 時、LLM 預設用 &lt;em>training 內的 latest fact&lt;/em> — 那個 fact 可能已過時。&lt;/p>
&lt;h3 id="2-llm-預設不標-claim-的時間性">2. LLM 預設不標 claim 的時間性&lt;/h3>
&lt;p>LLM 寫「PlanetScale 不支援 FK」、不會自動標「&lt;em>as of 2022&lt;/em>」、讀者看到 &lt;em>永久性 claim&lt;/em>。&lt;/p>
&lt;p>LLM 不會主動 verify「我寫的這個 claim 是 N 個月內仍 valid 的嗎」、除非寫作流程強制 verify step。&lt;/p>
&lt;h3 id="3-基於-claim-的整段流程是結構性-anchor">3. 基於 claim 的整段流程是「結構性 anchor」&lt;/h3>
&lt;p>Migration playbook 的 Phase 1 是 &lt;em>結構錨點&lt;/em> — 後續 Phase 2-4 都 reference Phase 1 結果。Phase 1 基於過時 claim 時、修法不只是 claim、是 &lt;em>整個 anchor 重做&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心vendor-feature-limitation-claim-有時間敏感性">核心：Vendor feature limitation claim 有時間敏感性</h2>
<p>寫 vendor article 時、常見以下 claim 形態：</p>
<ul>
<li>「Vendor X 不支援 Y」</li>
<li>「Vendor X 最多 Z」</li>
<li>「Vendor X 預設 W」</li>
</ul>
<p>這些 claim 在寫作那刻是真的、但 vendor 持續演進。寫作後 <em>N 個月</em> — 6 個月、12 個月、24 個月 — claim 可能反轉、整段 audit 邏輯 invalidates。</p>
<p>問題不只是 <em>claim 過時</em>、是 <em>基於 claim 的整段流程被推翻</em>。Migration playbook Phase 1 audit 如果以「Vendor 不支援 X」為前提、X 後來變支援、Phase 1 整段重寫。</p>
<h2 id="caseplanetscale-fk-claim-反轉">Case：PlanetScale FK claim 反轉</h2>
<p>寫 migrate-to-planetscale.md 跟 migrate-vitess-to-planetscale.md 時：</p>
<ul>
<li>Claim：「PlanetScale 不支援 Foreign Key（Vitess 限制）」</li>
<li>基於此 claim：Phase 1 audit 整段「FK audit + 全 drop FK + application enforcement 改寫」</li>
<li>Phase 1 是 weeks-months 工作量、第一個 phase</li>
</ul>
<p>實際狀態（4-reviewer C audit catch）：</p>
<ul>
<li>Vitess 18（2023 末）加 FK 支援</li>
<li>PlanetScale 2024 起在合適 plan 內可啟用 FK</li>
<li>「不支援」是 2022 年的事實、寫作時已過時</li>
</ul>
<p>修法：整段 Phase 1 audit 從「FK audit + drop」改寫成「FK 行為驗證 + cross-shard cascade 處理」。</p>
<p>這不是 <em>微調文字</em>、是 <em>整段 framing 重做</em>。</p>
<h2 id="機制為什麼會發生">機制：為什麼會發生</h2>
<h3 id="1-llm-training-cutoff-vs-vendor-changelog-速度差">1. LLM training cutoff vs vendor changelog 速度差</h3>
<p>LLM training data 有 cutoff date（通常滯後 12-18 個月）。Vendor major feature release 在 cutoff 後、LLM 不知道。</p>
<p>寫 vendor article 時、LLM 預設用 <em>training 內的 latest fact</em> — 那個 fact 可能已過時。</p>
<h3 id="2-llm-預設不標-claim-的時間性">2. LLM 預設不標 claim 的時間性</h3>
<p>LLM 寫「PlanetScale 不支援 FK」、不會自動標「<em>as of 2022</em>」、讀者看到 <em>永久性 claim</em>。</p>
<p>LLM 不會主動 verify「我寫的這個 claim 是 N 個月內仍 valid 的嗎」、除非寫作流程強制 verify step。</p>
<h3 id="3-基於-claim-的整段流程是結構性-anchor">3. 基於 claim 的整段流程是「結構性 anchor」</h3>
<p>Migration playbook 的 Phase 1 是 <em>結構錨點</em> — 後續 Phase 2-4 都 reference Phase 1 結果。Phase 1 基於過時 claim 時、修法不只是 claim、是 <em>整個 anchor 重做</em>。</p>
<p>這比修 isolated fact 工作量大 10x — 是「invalidates premise」、不是「fix typo」。</p>
<h3 id="4-vendor-article-多用-永久性語氣-而非-時間性語氣">4. Vendor article 多用 <em>永久性語氣</em> 而非 <em>時間性語氣</em></h3>
<p>寫作習慣寫「PlanetScale 不支援 FK」（永久性）、不寫「PlanetScale 截至 2022 末不支援 FK」（時間性）。</p>
<p>讀者讀到的是 <em>當前永久狀態</em>、寫作者其實只能保證 <em>寫作那刻</em>。</p>
<h2 id="修法">修法</h2>
<h3 id="1-每篇-vendor-article-標-last-verified-date">1. 每篇 vendor article 標 <code>Last verified</code> date</h3>
<p>frontmatter 或開頭加：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">last_verified</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-19</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">verified_against</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span>- <span class="l">PlanetScale docs（2026-05 access）</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span>- <span class="l">Vitess 18.0 release notes</span></span></span></code></pre></div><p>讓讀者看到 <em>寫作時 verify 的 source / date</em>、不假設永久性。</p>
<h3 id="2-feature-limitation-claim-加時間註">2. Feature limitation claim 加時間註</h3>
<p>寫「Vendor X 不支援 Y」時、加 <em>as of N</em>：</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">PlanetScale 截至 2024 末有限支援 FK（Vitess 18+、需明確啟用）</span></span></code></pre></div><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">PlanetScale 不支援 FK</span></span></code></pre></div><h3 id="3-claim-反轉--整段-audit-重寫不是-patch">3. Claim 反轉 → 整段 audit 重寫、不是 patch</h3>
<p>當 verify 發現 claim 已反轉（如 PlanetScale FK 從不支援變支援）、不要 <em>只改 claim 字句</em>。回頭看 <em>基於該 claim 的流程段落</em> —</p>
<ul>
<li>Migration Phase 1 audit</li>
<li>「何時不要遷」反向 recommendation</li>
<li>「跟 sibling vendor 對比」表</li>
</ul>
<p>每段都要 <em>重看是否還成立</em>、不成立的整段重寫。</p>
<h3 id="4-vendor-article-寫作前先-verify-主要-claim">4. Vendor article 寫作前先 verify 主要 claim</h3>
<p>寫作流程加 <em>verify checkpoint</em>：</p>
<ul>
<li>列出該 article 的「Vendor X 不支援 Y / 最多 Z / 預設 W」claim</li>
<li>對每個 claim、查 vendor official docs（最新 docs）/ recent release note（過去 12 個月）</li>
<li>不確定的標 <em>uncertain</em>、不要 confidence-fake</li>
</ul>
<h3 id="5-reviewer-c-必查-vendor-feature-time-sensitive-claim">5. Reviewer C 必查 vendor feature time-sensitive claim</h3>
<p>跑 4-reviewer audit 時、Reviewer C（技術準確性）必須：</p>
<ul>
<li>對每個 <em>feature limitation claim</em>、verify 是否仍 current</li>
<li>對每個 <em>vendor CLI command</em>、verify 是否真實存在（hallucinated CLI 是 sibling 問題）</li>
<li>對每個 <em>vendor default value</em>、verify 是否最新</li>
</ul>
<h2 id="hallucination-鄰近議題">Hallucination 鄰近議題</h2>
<p>LLM 寫 vendor CLI command 容易 hallucinate（例如 <code>pscale database promote-shadow</code>、<code>vtctldclient PartitionTablet</code>）— 命令不存在、是 LLM 編造。</p>
<p>跟本卡時間敏感性 <em>不完全相同</em> —</p>
<ul>
<li>時間敏感性：<em>claim 寫作時 valid、現在過時</em></li>
<li>Hallucination：<em>claim 寫作時也 invalid、是編造</em></li>
</ul>
<p>兩者修法部分重疊：</p>
<ul>
<li>寫前 verify（claim + CLI）</li>
<li>Reviewer C audit</li>
<li>不確定標 uncertain</li>
</ul>
<p>但 hallucination 是 <em>更基本的 verify failure</em>、本卡聚焦時間敏感性。</p>
<h2 id="跟既有原則的關係">跟既有原則的關係</h2>
<ul>
<li><a href="../sibling-coverage-asymmetry-blindspot-in-priority/">Sibling Coverage Asymmetry Blindspot in Priority</a>：本卡是 <em>claim 時間敏感性</em>、那卡是 <em>coverage 對稱性</em>、不同 axis</li>
<li><a href="../data-topology-as-audit-dimension/">Data Topology as Audit Dimension</a>：本卡是 <em>寫作 audit 應加時間維度</em>、那卡是 <em>content audit 應加 topology 維度</em></li>
</ul>
<h2 id="反向驗證">反向驗證</h2>
<p>不該誤用本卡：</p>
<ul>
<li><em>穩定 fact</em>（SQL syntax / RFC standard / industry-wide convention）不必標時間性、只有 <em>vendor-specific evolving feature</em> 才需要</li>
<li>不是每個 claim 都要 verify — 「MySQL replication 用 binlog」是穩定 fact、不必加 <em>as of N</em></li>
<li>過度標 <em>as of N</em> 會讓 article 變 verbose、只對 <em>limitation claim</em> 跟 <em>vendor-specific behavior</em> 套用</li>
</ul>
<h2 id="觸發再評估">觸發再評估</h2>
<p>未來累積到以下情境、本卡應 review：</p>
<ul>
<li>連續 2 個 batch 都踩 hallucinated CLI（trigger 升級到強制 <em>寫前 CLI verify</em>）</li>
<li>Feature claim 反轉 invalidates 整段流程的 case 超過 3 次（trigger 把 vendor article 改成 <em>每 N 個月 re-verify</em> 紀律）</li>
<li>LLM training cutoff 跟 vendor changelog 速度差變更大（trigger 升級 verify cadence）</li>
</ul>
]]></content:encoded></item><item><title>Cross-Reviewer Convergence：多 Reviewer 收斂的 finding 比單 Reviewer flag 信號強</title><link>https://tarrragon.github.io/blog/report/cross-reviewer-convergence-priority-weighting/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cross-reviewer-convergence-priority-weighting/</guid><description>&lt;h2 id="核心跨-reviewer-收斂的-finding-信號強">核心：跨 reviewer 收斂的 finding 信號強&lt;/h2>
&lt;p>當跑 multi-reviewer parallel audit（4-reviewer / N-reviewer）、最 high-priority 不是 &lt;em>單一 reviewer flag 的 most severe finding&lt;/em>、是 &lt;em>多個 reviewer 從不同軸獨立 flag 的同一 finding&lt;/em>。&lt;/p>
&lt;p>直覺：&lt;/p>
&lt;ul>
&lt;li>單 reviewer flag P0 finding 是 &lt;em>該軸的判斷&lt;/em>&lt;/li>
&lt;li>跨 reviewer convergence flag 是 &lt;em>多軸共同 hit 同一點&lt;/em>、信號收斂&lt;/li>
&lt;/ul>
&lt;p>機制：N 個獨立 axis 隨機 hit 同一 finding 的機率隨 N 指數下降 — 兩個 axis 偶然 hit 同點機率低、三個 axis hit 同點機率更低。所以 convergence 排除 &lt;em>單 reviewer 主觀 / 偏好 bias&lt;/em>、留 &lt;em>系統性 issue&lt;/em>。&lt;/p>
&lt;h2 id="casemysql-4-reviewer-audit">Case：MySQL 4-reviewer audit&lt;/h2>
&lt;p>跑 4-reviewer audit（A 寫作規範 / B 跨檔一致性 / C 技術準確性 / D 結構性質疑）對 MySQL 17 篇：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>Flagged by&lt;/th>
 &lt;th>Convergence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>4 篇 migration playbook 缺 weight + banner&lt;/td>
 &lt;td>Reviewer A + Reviewer B&lt;/td>
 &lt;td>&lt;strong>2 軸&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Frame uniformity（5 個踩雷 100% 重複）&lt;/td>
 &lt;td>Reviewer A + Reviewer D&lt;/td>
 &lt;td>&lt;strong>2 軸&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PlanetScale FK 過時 claim&lt;/td>
 &lt;td>Reviewer C 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PG CTE 版本錯（6.4 vs 8.4）&lt;/td>
 &lt;td>Reviewer C 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Connection memory 衝突（3MB vs 8-10MB）&lt;/td>
 &lt;td>Reviewer B 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework bias（Type A/C/E 集中）&lt;/td>
 &lt;td>Reviewer D 單獨&lt;/td>
 &lt;td>1 軸&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>2 軸 convergence 的 finding（缺 weight + frame uniformity）信號特別強 — 兩個 reviewer 從不同 audit 維度（寫作規範軸 vs 跨檔一致性軸）獨立判斷出同一 issue。&lt;/p>
&lt;p>對比：PlanetScale FK 是 &lt;em>單 reviewer 找到的 highest-severity finding&lt;/em>（invalidates 整段 Phase 1 audit premise）、但是 &lt;em>單軸 flag&lt;/em>。&lt;/p>
&lt;p>兩種都 P0、但 &lt;em>priority weighting&lt;/em> 應該不同：&lt;/p>
&lt;ul>
&lt;li>2 軸 convergence finding：&lt;em>structurally important&lt;/em>、是 batch level pattern&lt;/li>
&lt;li>單軸 high-severity finding：&lt;em>technically critical&lt;/em>、specific issue&lt;/li>
&lt;/ul>
&lt;h2 id="機制為什麼-convergence-比-severity-重要">機制：為什麼 convergence 比 severity 重要&lt;/h2>
&lt;h3 id="1-單-reviewer-flag-有-axis-specific-bias">1. 單 reviewer flag 有 axis-specific bias&lt;/h3>
&lt;p>每個 reviewer 用特定 audit 軸（寫作規範 / 一致性 / 技術 / 結構）。單軸 flag 帶該軸的 &lt;em>judgment preference&lt;/em>：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心跨-reviewer-收斂的-finding-信號強">核心：跨 reviewer 收斂的 finding 信號強</h2>
<p>當跑 multi-reviewer parallel audit（4-reviewer / N-reviewer）、最 high-priority 不是 <em>單一 reviewer flag 的 most severe finding</em>、是 <em>多個 reviewer 從不同軸獨立 flag 的同一 finding</em>。</p>
<p>直覺：</p>
<ul>
<li>單 reviewer flag P0 finding 是 <em>該軸的判斷</em></li>
<li>跨 reviewer convergence flag 是 <em>多軸共同 hit 同一點</em>、信號收斂</li>
</ul>
<p>機制：N 個獨立 axis 隨機 hit 同一 finding 的機率隨 N 指數下降 — 兩個 axis 偶然 hit 同點機率低、三個 axis hit 同點機率更低。所以 convergence 排除 <em>單 reviewer 主觀 / 偏好 bias</em>、留 <em>系統性 issue</em>。</p>
<h2 id="casemysql-4-reviewer-audit">Case：MySQL 4-reviewer audit</h2>
<p>跑 4-reviewer audit（A 寫作規範 / B 跨檔一致性 / C 技術準確性 / D 結構性質疑）對 MySQL 17 篇：</p>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>Flagged by</th>
          <th>Convergence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>4 篇 migration playbook 缺 weight + banner</td>
          <td>Reviewer A + Reviewer B</td>
          <td><strong>2 軸</strong></td>
      </tr>
      <tr>
          <td>Frame uniformity（5 個踩雷 100% 重複）</td>
          <td>Reviewer A + Reviewer D</td>
          <td><strong>2 軸</strong></td>
      </tr>
      <tr>
          <td>PlanetScale FK 過時 claim</td>
          <td>Reviewer C 單獨</td>
          <td>1 軸</td>
      </tr>
      <tr>
          <td>PG CTE 版本錯（6.4 vs 8.4）</td>
          <td>Reviewer C 單獨</td>
          <td>1 軸</td>
      </tr>
      <tr>
          <td>Connection memory 衝突（3MB vs 8-10MB）</td>
          <td>Reviewer B 單獨</td>
          <td>1 軸</td>
      </tr>
      <tr>
          <td>Framework bias（Type A/C/E 集中）</td>
          <td>Reviewer D 單獨</td>
          <td>1 軸</td>
      </tr>
  </tbody>
</table>
<p>2 軸 convergence 的 finding（缺 weight + frame uniformity）信號特別強 — 兩個 reviewer 從不同 audit 維度（寫作規範軸 vs 跨檔一致性軸）獨立判斷出同一 issue。</p>
<p>對比：PlanetScale FK 是 <em>單 reviewer 找到的 highest-severity finding</em>（invalidates 整段 Phase 1 audit premise）、但是 <em>單軸 flag</em>。</p>
<p>兩種都 P0、但 <em>priority weighting</em> 應該不同：</p>
<ul>
<li>2 軸 convergence finding：<em>structurally important</em>、是 batch level pattern</li>
<li>單軸 high-severity finding：<em>technically critical</em>、specific issue</li>
</ul>
<h2 id="機制為什麼-convergence-比-severity-重要">機制：為什麼 convergence 比 severity 重要</h2>
<h3 id="1-單-reviewer-flag-有-axis-specific-bias">1. 單 reviewer flag 有 axis-specific bias</h3>
<p>每個 reviewer 用特定 audit 軸（寫作規範 / 一致性 / 技術 / 結構）。單軸 flag 帶該軸的 <em>judgment preference</em>：</p>
<ul>
<li>Reviewer A 偏好 <em>寫作風格規範</em>、可能 flag 過嚴</li>
<li>Reviewer C 偏好 <em>technical correctness</em>、可能 flag 一些 <em>正確但 niche</em> 議題</li>
</ul>
<p>單軸 flag finding 可能是 <em>該軸 perspective 的 P0、其他軸 perspective 不重要</em>。</p>
<h3 id="2-跨-axis-convergence-排除-axis-specific-bias">2. 跨 axis convergence 排除 axis-specific bias</h3>
<p>當兩個 reviewer 從 <em>不同 axis</em> 獨立 flag 同 finding、表示這個 issue 對 <em>多種 judgment perspective</em> 都 reachable — 是 <em>系統性 pattern</em>、不是單一 perspective 的偏好。</p>
<p>舉例：「4 篇 migration playbook 缺 weight」</p>
<ul>
<li>Reviewer A 從 <em>寫作規範</em> 角度 flag：missing frontmatter required field</li>
<li>Reviewer B 從 <em>跨檔一致性</em> 角度 flag：13 篇 deep article 有 weight、4 篇 migration 沒有、不對齊</li>
</ul>
<p>兩個獨立 reasoning path 到同一 finding、信號收斂、是 <em>結構性問題</em>。</p>
<h3 id="3-convergence-finding-修一次解決多-reviewer-flag">3. Convergence finding 修一次解決多 reviewer flag</h3>
<p>實作層：</p>
<ul>
<li>單軸 P0：修 → 解決 1 個 reviewer 的 flag</li>
<li>雙軸 convergence：修 → 解決 2 個 reviewer 的 flag</li>
</ul>
<p>ROI 上 convergence finding 修法效率 2x。</p>
<h3 id="4-convergence-揭露-audit-framework-blindspot-的補集">4. Convergence 揭露 audit framework blindspot 的補集</h3>
<p>如果某 finding <em>所有 reviewer 都沒 flag</em>、可能：</p>
<ul>
<li>沒問題（true negative）</li>
<li>所有 axis 都看不到（structural blindspot）</li>
</ul>
<p>如果某 finding <em>只一 reviewer flag</em>、可能：</p>
<ul>
<li>Niche but real（axis-specific catch）</li>
<li>Axis-specific bias</li>
</ul>
<p>如果某 finding <em>多 reviewer flag</em>、強：</p>
<ul>
<li>多 axis 收斂 → 高度 likely true positive</li>
<li>排除 axis-specific bias</li>
</ul>
<h2 id="修法cross-reviewer-convergence-matrix">修法：Cross-reviewer convergence matrix</h2>
<h3 id="1-multi-reviewer-audit-後做-convergence-matrix">1. Multi-reviewer audit 後做 convergence matrix</h3>
<p>收齊 N 個 reviewer report 後、不是 merge findings list、是建 matrix：</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">Finding          | Reviewer A | Reviewer B | Reviewer C | Reviewer D | Convergence
</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">Missing weight   |     P0     |     P0     |            |            |    **2**
</span></span><span class="line"><span class="ln">4</span><span class="cl">Frame uniformity |     P1     |            |            |     -      |    **2**
</span></span><span class="line"><span class="ln">5</span><span class="cl">FK claim 過時    |            |            |     P0     |            |    1
</span></span><span class="line"><span class="ln">6</span><span class="cl">CTE version 錯   |            |            |     P0     |            |    1
</span></span><span class="line"><span class="ln">7</span><span class="cl">Conn memory 衝突 |            |     P0     |            |            |    1</span></span></code></pre></div><p>Convergence column 自動標 priority bump — 2+ 列為 <em>首要 fix</em>、1 列為 <em>依 severity 處理</em>。</p>
<h3 id="2-priority-list-按-convergence-排序不是純按-severity">2. Priority list 按 convergence 排序、不是純按 severity</h3>
<p>修法 priority：</p>
<ol>
<li><strong>2+ convergence finding</strong>（系統性 pattern）— 必修、高 ROI</li>
<li><strong>單軸 + 高 severity finding</strong>（如 FK claim 過時 invalidates premise）— 必修、specific</li>
<li><strong>單軸 + 中 severity finding</strong>（如 CTE version 錯）— 修、ROI 中等</li>
<li><strong>單軸 + 低 severity finding</strong> — 可選</li>
</ol>
<h3 id="3-convergence-揭露的-pattern-寫進-retro">3. Convergence 揭露的 <em>pattern</em> 寫進 retro</h3>
<p>2+ convergence finding 通常是 <em>寫作流程 / 模板</em> 級議題、修了該 case 還要回頭看 <em>為什麼會系統性發生</em>：</p>
<ul>
<li>Missing weight：寫 migration playbook 模板沒有 weight、是 <em>template gap</em></li>
<li>Frame uniformity：「5 個踩雷」frame 在所有 article 重複、是 <em>frame template too rigid</em></li>
</ul>
<p>把這些 pattern 寫進 retro / report card、未來不再踩。</p>
<h2 id="跟既有原則的關係">跟既有原則的關係</h2>
<ul>
<li><a href="../sibling-coverage-asymmetry-blindspot-in-priority/">Sibling Coverage Asymmetry Blindspot in Priority</a>：本卡是 <em>audit finding 的 priority weighting</em>、那卡是 <em>batch coverage 的 priority weighting</em>、不同 layer</li>
<li><a href="../multi-pass-review-frame-granularity-blindspot/">Multi-Pass Review Frame Granularity Blindspot</a>：multi-pass 是 <em>同 reviewer 多輪</em>、本卡是 <em>多 reviewer 平行</em>、不同模式</li>
</ul>
<h2 id="反向驗證">反向驗證</h2>
<p>不該誤用：</p>
<ul>
<li><em>Convergence &gt; severity</em> 不是絕對 — 單軸高 severity finding（如 invalidates premise）仍是必修、不該因為「只一軸 flag」延後</li>
<li>N=1 reviewer audit 不適用本卡 — 至少 2 個 reviewer 才有 convergence 概念</li>
<li>2 個 reviewer 用 <em>同樣 axis</em> 都 flag 不算 convergence — 必須 <em>不同 axis</em> 才是真正收斂</li>
<li>Reviewer 之間 <em>互相看過彼此 report</em> 後再 flag 不算 convergence — 必須 <em>獨立 parallel</em> 跑</li>
</ul>
<h2 id="觸發再評估">觸發再評估</h2>
<ul>
<li>N-reviewer audit 跑超過 5 輪後、check convergence finding 的 follow-up rate 是否真比單軸 finding 高</li>
<li>出現 <em>3 軸以上 convergence</em> 的 finding 時、是否 trigger framework-level review（不只是 content fix）</li>
<li>累積足夠 reviewer convergence case 後、考慮抽出 <em>axis design 原則</em>：哪些 axis 組合的 convergence 最 informative</li>
</ul>
]]></content:encoded></item><item><title>新增頂層 content 資料夾要同步首頁 _index.md 入口</title><link>https://tarrragon.github.io/blog/report/top-level-content-folder-needs-homepage-entry/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/top-level-content-folder-needs-homepage-entry/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>新增頂層 content 資料夾的同個 commit、必須同步把該模組入口加進首頁 &lt;code>content/_index.md&lt;/code> 的對應分類段。Hugo 不會自動把 content/ 下的頂層目錄列在首頁、首頁完全靠 &lt;code>content/_index.md&lt;/code> 的 markdown 內容渲染。模組內部建得再完整、首頁沒入口 = 該模組對新讀者不可發現。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>動作&lt;/th>
 &lt;th>是否會在首頁顯示&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>建立 &lt;code>content/&amp;lt;module&amp;gt;/_index.md&lt;/code>&lt;/td>
 &lt;td>不會、Hugo 不 auto-list 頂層目錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在 &lt;code>hugo.toml&lt;/code> 加 &lt;code>[[menu.main]]&lt;/code> entry&lt;/td>
 &lt;td>顯示在頂部選單、跟首頁清單無關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在 &lt;code>content/_index.md&lt;/code> 對應分類段加 markdown link&lt;/td>
 &lt;td>顯示在首頁&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「&lt;strong>新讀者從首頁進來、能不能 1-2 個 click 走到該模組？&lt;/strong>」答案是「不能」就是入口斷裂。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>建立新教學模組（例：&lt;code>content/business/&lt;/code>）走 case-first 完整流程：寫 &lt;code>_index.md&lt;/code>、批次建 knowledge-cards、補 reading-frameworks、寫 case-analyses 文章、跑 mdtools 三項檢查、commit + push。模組內部 50 個檔案全綠、卡片網路雙向連結完整、case-first + agent team review 跑完。&lt;/p>
&lt;p>問題是首頁 &lt;code>content/_index.md&lt;/code> 的「教學系列」段沒同步更新、business 模組從首頁進入的入口完全缺席。讀者反饋「我在 blog 首頁沒看到商業這個分類」之前、新模組對 organic traffic 是隱形的。&lt;/p>
&lt;p>具體 case：c2c01bf 建立 &lt;code>content/business/&lt;/code> 50 個檔案、但 &lt;code>content/_index.md&lt;/code> 的 &lt;code>## 教學系列&lt;/code> 段沒加 &lt;code>[商業概念與策略分析](/business/)&lt;/code> 入口；f665e6d 才補上。這個 gap 在過去 backend / llm / ci 等模組可能也遇過、但當時由建立者順手補上、沒成為被紀錄的 retrospective、所以 pattern 沒浮現。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步把新增頂層資料夾跟首頁入口綁成同一個-commit">第一步：把「新增頂層資料夾」跟「首頁入口」綁成同一個 commit&lt;/h3>
&lt;p>寫新模組的 PR / commit 時、把 &lt;code>content/_index.md&lt;/code> 的修改一起放進去。Commit message 明確點出兩件事：&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">business: 新增商業教材模組 + 首頁入口
&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">- content/business/ 50 檔
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">- content/_index.md 教學系列段加入口&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>避免拆兩個 commit、避免「下個 PR 再補」。&lt;/p>
&lt;h3 id="第二步完稿檢查清單加一條">第二步：完稿檢查清單加一條&lt;/h3>
&lt;p>AGENTS.md 完稿檢查清單明列：&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">- [ ] 新增頂層 content/&amp;lt;module&amp;gt;/ 資料夾時、已同步更新 content/_index.md 對應分類段&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寫完模組準備 commit 前、過 checklist 時會被 catch。&lt;/p>
&lt;h3 id="第三步考慮工具化可選不強制">第三步：考慮工具化（可選、不強制）&lt;/h3>
&lt;p>長期可寫 mdtools check：列出 &lt;code>content/&lt;/code> 頂層資料夾、跟 &lt;code>content/_index.md&lt;/code> 的 markdown link 比對、缺的警告。但這個 check 有 false positive 風險（不是所有資料夾都該上首頁、例如 &lt;code>record&lt;/code> / &lt;code>report&lt;/code> / &lt;code>work-log&lt;/code> 是已存在但獨立的 surface、&lt;code>tags&lt;/code> 是 Hugo 自動產生），實作前先設計 whitelist 機制。建議優先靠 checklist、不急著工具化。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="新模組對首頁讀者完全不可發現">新模組對首頁讀者完全不可發現&lt;/h3>
&lt;p>讀者從 blog 首頁進來、看到的是 &lt;code>content/_index.md&lt;/code> 渲染的內容。新模組沒在那個 markdown 裡 = 該模組對 organic traffic 完全消失。內部結構建得再好、卡片再多、case 寫得再深、新讀者進不來 = 等於沒上線。Search 跟 sitemap 是備援、不是主要入口。&lt;/p>
&lt;h3 id="補救拆成第二個-commit歷史變零碎">補救拆成第二個 commit、歷史變零碎&lt;/h3>
&lt;p>漏掉的入口要事後補、commit 歷史會變成「建模組」+「補首頁入口」兩個 commit。如果在多個 PR 之間，補入口 commit 容易被誤判為瑣碎的 typo fix、reviewer 不會深究、變成 silent fix。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>新增頂層 content 資料夾的同個 commit、必須同步把該模組入口加進首頁 <code>content/_index.md</code> 的對應分類段。Hugo 不會自動把 content/ 下的頂層目錄列在首頁、首頁完全靠 <code>content/_index.md</code> 的 markdown 內容渲染。模組內部建得再完整、首頁沒入口 = 該模組對新讀者不可發現。</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>是否會在首頁顯示</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建立 <code>content/&lt;module&gt;/_index.md</code></td>
          <td>不會、Hugo 不 auto-list 頂層目錄</td>
      </tr>
      <tr>
          <td>在 <code>hugo.toml</code> 加 <code>[[menu.main]]</code> entry</td>
          <td>顯示在頂部選單、跟首頁清單無關</td>
      </tr>
      <tr>
          <td>在 <code>content/_index.md</code> 對應分類段加 markdown link</td>
          <td>顯示在首頁</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「<strong>新讀者從首頁進來、能不能 1-2 個 click 走到該模組？</strong>」答案是「不能」就是入口斷裂。</p>
<hr>
<h2 id="情境">情境</h2>
<p>建立新教學模組（例：<code>content/business/</code>）走 case-first 完整流程：寫 <code>_index.md</code>、批次建 knowledge-cards、補 reading-frameworks、寫 case-analyses 文章、跑 mdtools 三項檢查、commit + push。模組內部 50 個檔案全綠、卡片網路雙向連結完整、case-first + agent team review 跑完。</p>
<p>問題是首頁 <code>content/_index.md</code> 的「教學系列」段沒同步更新、business 模組從首頁進入的入口完全缺席。讀者反饋「我在 blog 首頁沒看到商業這個分類」之前、新模組對 organic traffic 是隱形的。</p>
<p>具體 case：c2c01bf 建立 <code>content/business/</code> 50 個檔案、但 <code>content/_index.md</code> 的 <code>## 教學系列</code> 段沒加 <code>[商業概念與策略分析](/business/)</code> 入口；f665e6d 才補上。這個 gap 在過去 backend / llm / ci 等模組可能也遇過、但當時由建立者順手補上、沒成為被紀錄的 retrospective、所以 pattern 沒浮現。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步把新增頂層資料夾跟首頁入口綁成同一個-commit">第一步：把「新增頂層資料夾」跟「首頁入口」綁成同一個 commit</h3>
<p>寫新模組的 PR / commit 時、把 <code>content/_index.md</code> 的修改一起放進去。Commit message 明確點出兩件事：</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">business: 新增商業教材模組 + 首頁入口
</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">- content/business/ 50 檔
</span></span><span class="line"><span class="ln">4</span><span class="cl">- content/_index.md 教學系列段加入口</span></span></code></pre></div><p>避免拆兩個 commit、避免「下個 PR 再補」。</p>
<h3 id="第二步完稿檢查清單加一條">第二步：完稿檢查清單加一條</h3>
<p>AGENTS.md 完稿檢查清單明列：</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">- [ ] 新增頂層 content/&lt;module&gt;/ 資料夾時、已同步更新 content/_index.md 對應分類段</span></span></code></pre></div><p>寫完模組準備 commit 前、過 checklist 時會被 catch。</p>
<h3 id="第三步考慮工具化可選不強制">第三步：考慮工具化（可選、不強制）</h3>
<p>長期可寫 mdtools check：列出 <code>content/</code> 頂層資料夾、跟 <code>content/_index.md</code> 的 markdown link 比對、缺的警告。但這個 check 有 false positive 風險（不是所有資料夾都該上首頁、例如 <code>record</code> / <code>report</code> / <code>work-log</code> 是已存在但獨立的 surface、<code>tags</code> 是 Hugo 自動產生），實作前先設計 whitelist 機制。建議優先靠 checklist、不急著工具化。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="新模組對首頁讀者完全不可發現">新模組對首頁讀者完全不可發現</h3>
<p>讀者從 blog 首頁進來、看到的是 <code>content/_index.md</code> 渲染的內容。新模組沒在那個 markdown 裡 = 該模組對 organic traffic 完全消失。內部結構建得再好、卡片再多、case 寫得再深、新讀者進不來 = 等於沒上線。Search 跟 sitemap 是備援、不是主要入口。</p>
<h3 id="補救拆成第二個-commit歷史變零碎">補救拆成第二個 commit、歷史變零碎</h3>
<p>漏掉的入口要事後補、commit 歷史會變成「建模組」+「補首頁入口」兩個 commit。如果在多個 PR 之間，補入口 commit 容易被誤判為瑣碎的 typo fix、reviewer 不會深究、變成 silent fix。</p>
<h3 id="pattern-不紀錄每個新模組都重蹈覆轍">Pattern 不紀錄、每個新模組都重蹈覆轍</h3>
<p>backend / llm / ci 等過去新模組可能也遇過相同 gap、但當時被建立者順手補了、沒成為被紀錄的 retrospective。Pattern 不浮現 = 下個建模組者重蹈覆轍。本卡的責任就是把這個 pattern 紀錄下來、進完稿檢查清單後變成結構性保證。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../single-source-of-truth/">#44 Single Source of Truth</a></strong>：本卡是 #44 在「首頁清單」維度的具體案例。<code>content/_index.md</code> 的「教學系列」段是讀者入口的 SSoT、不是 auto-derived from filesystem。建立模組時這個 SSoT 沒同步 = SSoT 違反。</li>
<li><strong><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></strong>：本卡是 #97 在「上一層 surface」的具體形態。一個模組的 metadata surface 不只是它自己的 title / description / heading、還包括「上一層索引怎麼提它」— 首頁清單就是 module 的上層 metadata surface、跟模組內 metadata 一樣值得納入 review。</li>
<li><strong><a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證</a></strong>：本卡是 #131 在「讀者旅程起點」的具體 gotcha。讀者旅程的入口是首頁、首頁沒 link 等於旅程斷在 step 0。模組內部教學完整性再高、入口斷裂就是 0-completion。</li>
<li><strong><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass scope 要蓋同類風險區</a></strong>：本卡跟 #95 的關係是「同類風險區包括上游引用點」。寫新模組時 scope 不只限模組內部、要涵蓋上游引用該模組的所有位置（首頁 / sibling 模組 _index.md cross-link / <code>AGENTS.md</code> / <code>CLAUDE.md</code>）。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commit 建了新頂層 <code>content/&lt;module&gt;/</code> 但沒動 <code>content/_index.md</code></td>
          <td>check 首頁入口、補同 commit 或下個 commit 立刻補 + 寫 retro 卡</td>
      </tr>
      <tr>
          <td>讀者反饋「我在首頁沒看到 X」</td>
          <td>too late、應在建模組同 commit 修；同步紀錄 retro 確認 pattern</td>
      </tr>
      <tr>
          <td>不確定首頁清單是否自動產生</td>
          <td>不是、<code>content/_index.md</code> 是手動 markdown、Hugo 不 auto-list</td>
      </tr>
      <tr>
          <td>完稿檢查清單沒列「同步首頁入口」</td>
          <td>補進 <code>AGENTS.md</code> 完稿檢查清單</td>
      </tr>
      <tr>
          <td>Sibling 模組 <code>_index.md</code> 提到新模組時沒 cross-link</td>
          <td>同類風險、scope 要蓋所有上游引用點、不只首頁</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用範圍</strong>：
<ul>
<li>新增 <code>content/&lt;module&gt;/</code> 頂層教學資料夾（Backend / LLM / CI / Business / 未來新模組）</li>
<li>把既有頂層資料夾「升格」成正式教學系列（例如從草稿區抽出獨立 surface）</li>
</ul>
</li>
<li><strong>不適用</strong>：
<ul>
<li>新增現有模組的子章節（例如 <code>content/backend/01-database/</code> 下新檔）— 這由模組自己的 <code>_index.md</code> 路由、不涉及首頁</li>
<li>新增 <code>content/posts/</code> 下單篇文章 — posts 是 chronological feed、首頁分類段不負責列每篇文章</li>
<li>新增 <code>content/work-log/</code> 或 <code>content/report/</code> 下單張卡片 — 同上、這些是已建立的 surface、清單在各自 <code>_index.md</code></li>
</ul>
</li>
<li><strong>邊界</strong>：本卡只處理「首頁入口」一個 surface；其他上游引用點（例如 sibling 模組 <code>_index.md</code> 提及本模組、<code>AGENTS.md</code> 提及本模組、<code>CLAUDE.md</code> 提及本模組）是同 pattern 的延伸、但個別 cross-link 要靠該位置自己的 review 維護</li>
</ul>
]]></content:encoded></item><item><title>WRAP Widen Options 容易塌成稻草人 framing、要改 evidence weight 結構</title><link>https://tarrragon.github.io/blog/report/wrap-widen-options-strawman-risk/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/wrap-widen-options-strawman-risk/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>WRAP 框架的 Widen Options 段落是「探索本質不同的因果解釋」、不是「列出競爭性派系然後打掉錯的」。當寫作時把「Widen Options → Reality Test」當作全文 narrative 主軸、整個 hypothesis space 探索就會塌成「兩弱一強稻草人」結構—A、B 是 dummy、C 永遠預設正解、Reality Test 用來證明 C。讀者第一遍可能不發現、第二遍就會看穿是修辭、不是真實的選項擴增。&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>Widen Options&lt;/td>
 &lt;td>A 派 / B 派 / C 派、C 永遠正解&lt;/td>
 &lt;td>解釋 (1) / (2) / (3)、每個有 prior + testable prediction&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reality Test&lt;/td>
 &lt;td>「A 不成立、B 不成立、C 成立」三連否定&lt;/td>
 &lt;td>Evidence weight assessment、各配「強 / 中 / 弱」訊號 + 估佔比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結論&lt;/td>
 &lt;td>「正確解釋是 C」winner-takes-all&lt;/td>
 &lt;td>「主因 X / 次因 Y / 邊際 Z」多解釋並存配 Falsifier&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「刪掉 Reality Test 那段、單看 Widen Options 那段、讀者能不能猜出哪個是正解？」能猜出 = 稻草人結構、不能猜出 = 真正的 widen。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>寫商業 case-analyses 套 WRAP 框架時、最容易踩的陷阱是把 Widen Options 寫成派系命名 + 預設正解、然後 Reality Test 一條一條打掉。寫作者的心理路徑很自然：先有觀點 → 為了「公平」列出反方 → 為了「驗證」打掉反方 → 留下原本就要說的觀點。這個流程跑完、Widen Options 段落變成修辭裝飾、不是真正的 hypothesis space 探索。&lt;/p>
&lt;p>具體 case（2026-05-20、e00253c 修法前）：blog 寫 3 篇 case-analyses（&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/claude-for-legal/" data-link-title="Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓" data-link-desc="用 WRAP 框架拆解基礎模型供應商進入垂直市場觸發的三層結構轉變：應用層 SaaS 毛利擠壓、新創淘汰、知識工作者判斷賭注放大">Claude for Legal&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/business/case-analyses/fde-arms-race/" data-link-title="FDE 軍備競賽：SaaS 支柱鬆動下的結構性轉變" data-link-desc="用 WRAP 框架拆解三家基礎模型供應商同時押 FDE 模式背後的 SaaS 商業前提鬆動，並判讀 FDE 是過渡狀態還是長期結構">FDE 軍備競賽&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/business/case-analyses/bufstream-acquisition/" data-link-title="CoreWeave 收購 Bufstream：整併週期下的賽道判讀與基礎設施重組" data-link-desc="用 WRAP 框架拆解 CoreWeave 買 Bufstream 揭露的串流市場整併、算力廠商對基礎設施的剛需、以及對資料工程師職涯的意涵">CoreWeave 收購 Bufstream&lt;/a>）、3 篇全部踩同一個結構：&lt;/p>
&lt;ul>
&lt;li>claude-for-legal：A「AWS 類比派」/ B「保護傘派」/ C「Enterprise Lock-in 派」、C 預設正解&lt;/li>
&lt;li>fde-arms-race：A「模仿派」/ B「策略選擇派」/ C「結構性被迫派」、C 預設正解&lt;/li>
&lt;li>bufstream：對 CoreWeave 為什麼買、X「業務擴張」/ Y「技術自主」、Y 預設正解&lt;/li>
&lt;/ul>
&lt;p>3-reviewer audit 平行跑、3 個 reviewer 獨立都點出「兩弱一強稻草人」結構共通—診斷一致是這個 pattern 不是個別失誤、是 WRAP 套案例寫作的系統性陷阱。e00253c 重寫後改為「解釋 (1) / (2) / (3) / (4) 並陳因果鏈、每個有 prior + prediction」、Reality Test 改為 evidence weight assessment + Falsifier、Round 2 驗證通過。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>WRAP 框架的 Widen Options 段落是「探索本質不同的因果解釋」、不是「列出競爭性派系然後打掉錯的」。當寫作時把「Widen Options → Reality Test」當作全文 narrative 主軸、整個 hypothesis space 探索就會塌成「兩弱一強稻草人」結構—A、B 是 dummy、C 永遠預設正解、Reality Test 用來證明 C。讀者第一遍可能不發現、第二遍就會看穿是修辭、不是真實的選項擴增。</p>
<table>
  <thead>
      <tr>
          <th>段落責任</th>
          <th>塌成稻草人時的症狀</th>
          <th>正確的形態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Widen Options</td>
          <td>A 派 / B 派 / C 派、C 永遠正解</td>
          <td>解釋 (1) / (2) / (3)、每個有 prior + testable prediction</td>
      </tr>
      <tr>
          <td>Reality Test</td>
          <td>「A 不成立、B 不成立、C 成立」三連否定</td>
          <td>Evidence weight assessment、各配「強 / 中 / 弱」訊號 + 估佔比</td>
      </tr>
      <tr>
          <td>結論</td>
          <td>「正確解釋是 C」winner-takes-all</td>
          <td>「主因 X / 次因 Y / 邊際 Z」多解釋並存配 Falsifier</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「刪掉 Reality Test 那段、單看 Widen Options 那段、讀者能不能猜出哪個是正解？」能猜出 = 稻草人結構、不能猜出 = 真正的 widen。</p>
<hr>
<h2 id="情境">情境</h2>
<p>寫商業 case-analyses 套 WRAP 框架時、最容易踩的陷阱是把 Widen Options 寫成派系命名 + 預設正解、然後 Reality Test 一條一條打掉。寫作者的心理路徑很自然：先有觀點 → 為了「公平」列出反方 → 為了「驗證」打掉反方 → 留下原本就要說的觀點。這個流程跑完、Widen Options 段落變成修辭裝飾、不是真正的 hypothesis space 探索。</p>
<p>具體 case（2026-05-20、e00253c 修法前）：blog 寫 3 篇 case-analyses（<a href="/blog/business/case-analyses/claude-for-legal/" data-link-title="Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓" data-link-desc="用 WRAP 框架拆解基礎模型供應商進入垂直市場觸發的三層結構轉變：應用層 SaaS 毛利擠壓、新創淘汰、知識工作者判斷賭注放大">Claude for Legal</a> / <a href="/blog/business/case-analyses/fde-arms-race/" data-link-title="FDE 軍備競賽：SaaS 支柱鬆動下的結構性轉變" data-link-desc="用 WRAP 框架拆解三家基礎模型供應商同時押 FDE 模式背後的 SaaS 商業前提鬆動，並判讀 FDE 是過渡狀態還是長期結構">FDE 軍備競賽</a> / <a href="/blog/business/case-analyses/bufstream-acquisition/" data-link-title="CoreWeave 收購 Bufstream：整併週期下的賽道判讀與基礎設施重組" data-link-desc="用 WRAP 框架拆解 CoreWeave 買 Bufstream 揭露的串流市場整併、算力廠商對基礎設施的剛需、以及對資料工程師職涯的意涵">CoreWeave 收購 Bufstream</a>）、3 篇全部踩同一個結構：</p>
<ul>
<li>claude-for-legal：A「AWS 類比派」/ B「保護傘派」/ C「Enterprise Lock-in 派」、C 預設正解</li>
<li>fde-arms-race：A「模仿派」/ B「策略選擇派」/ C「結構性被迫派」、C 預設正解</li>
<li>bufstream：對 CoreWeave 為什麼買、X「業務擴張」/ Y「技術自主」、Y 預設正解</li>
</ul>
<p>3-reviewer audit 平行跑、3 個 reviewer 獨立都點出「兩弱一強稻草人」結構共通—診斷一致是這個 pattern 不是個別失誤、是 WRAP 套案例寫作的系統性陷阱。e00253c 重寫後改為「解釋 (1) / (2) / (3) / (4) 並陳因果鏈、每個有 prior + prediction」、Reality Test 改為 evidence weight assessment + Falsifier、Round 2 驗證通過。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步widen-options-改成並陳的合理因果鏈不是派系命名">第一步：Widen Options 改成「並陳的合理因果鏈」、不是派系命名</h3>
<p>每個選項要滿足四個判準：</p>
<ol>
<li><strong>有實際 prior</strong>：誰持這論、為何合理。Prior 可以引用 VC / 創辦人 / 學者 / 業界分析師的公開立場、或從產業結構推導出的合理初始假設。</li>
<li><strong>有 testable prediction</strong>：若這個解釋成立、會看到什麼具體 evidence（合約規模、客戶分布、銷售節奏、員工流向）。</li>
<li><strong>跟其他選項的因果鏈本質不同</strong>：不是同一個結論的不同包裝、是不同的因果起點。</li>
<li><strong>不是設定就要被打爆的 dummy</strong>：選項要站得住、要可被讀者挑戰、不是 reductio ad absurdum 的設定。</li>
</ol>
<p>選項命名用中性編號「解釋 (1) / (2) / (3)」、避免「X 派 / Y 派 / Z 派」派系暗示—派系命名自帶「選邊」修辭框架。</p>
<h3 id="第二步reality-test-改成-evidence-based-weight-assessment">第二步：Reality Test 改成 evidence-based weight assessment</h3>
<p>不是「A 不成立、B 不成立、C 成立」三連否定、而是給每個解釋配 evidence 強度 + 估佔比百分比：</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)：強訊號（觀察 X、Y、Z 都支持）— 估佔比 ~50%
</span></span><span class="line"><span class="ln">2</span><span class="cl">解釋 (2)：中等訊號（觀察 W 支持、觀察 V 部分反駁）— 估佔比 ~30%
</span></span><span class="line"><span class="ln">3</span><span class="cl">解釋 (3)：弱訊號（難排除巧合）— 估佔比 ~20%</span></span></code></pre></div><p>允許多選項並存、用「主因 / 次因 / 邊際」權重判讀、不是 winner-takes-all。最後綜合判讀用「主要承擔 A 功能、伴隨 B 跟 C 兩個次要動機」這類多解釋並存的措辭。</p>
<h3 id="第三步補-falsifier-段列出每個解釋的反證訊號">第三步：補 Falsifier 段、列出每個解釋的反證訊號</h3>
<p>每個解釋配對應的 Falsifier：「若觀察到 X、解釋 (N) 主導論垮、要重評估」。這跟 Tripwire 段銜接、形成可監控的判讀結構。Falsifier 不是「整套論述崩」、是 partial revision—某個解釋的權重變化、不一定推翻整個分析框架。</p>
<h3 id="第四步完稿時跑刪-reality-test-測試">第四步：完稿時跑「刪 Reality Test 測試」</h3>
<p>寫完後、心裡 simulate「刪掉 Reality Test 那段、讀者單看 Widen Options 那段能不能猜出哪個是正解」。能猜出就是稻草人結構、需要重寫 Widen Options 讓選項真的並陳。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="修辭被讀穿信任度下降">修辭被讀穿、信任度下降</h3>
<p>讀者一旦看穿稻草人結構、會懷疑作者的真誠度跟結論可靠度。教學文章靠「分析嚴謹」立信、稻草人結構直接破壞這個基礎。本 blog 的 3-reviewer audit 之所以能 3 個 reviewer 獨立都 catch 到同一個 pattern、就是因為這個結構在 careful reader 眼中明顯—第一篇可能蒙混、第三篇就會被當成 systematic 修辭技倆。</p>
<h3 id="hypothesis-space-探索失效判讀框架不可遷移">Hypothesis space 探索失效、判讀框架不可遷移</h3>
<p>WRAP 的核心價值是「強制做 hypothesis space 探索、防止認知偏誤」、稻草人結構讓 Widen Options 退化成修辭裝飾、原本要解的「直覺接受第一個解釋」問題沒被解。讀者拿走的判讀框架也不可遷移到下次事件—因為框架的應用方式本身就是錯的、套到新事件還是會列稻草人。</p>
<h3 id="tone-滑成-opinion-piece">Tone 滑成 opinion piece</h3>
<p>「我列出爛選項打掉、留下正解」的結構帶有「我來糾正你」的姿態、整篇 register 從教學滑向 opinion piece。即使每個字句都中性、整體 tone 仍會偏。本 blog case-analyses 重寫前的 register 評估是 opinion 40% / blog 30% / teaching 20% / academic 10%、重寫後翻轉到 teaching 55-60% / academic 25-35% / blog ≤ 15%—單純改 framing 結構就能整體位移 register。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>
<p><strong><a href="../collapse-is-implicit-default/">#125 Collapse 是隱形預設</a></strong>：本卡是 #125 在「WRAP Widen Options」surface 的具體實例。Widen Options 塌成「兩弱一強」是把高維 hypothesis space collapse 到便利的「正解 vs 稻草人」二維。修法是預設展開多選項、選窄要 evidence 支持、不是修辭便利。</p>
</li>
<li>
<p><strong><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></strong>：sister 卡。#79 是 decision-making 多軸、本卡是 WRAP 寫作多軸；兩者結構同骨—Widen Options 不能塌成 2 維、就像 decision dialogue 不能塌成單軸。</p>
</li>
<li>
<p><strong><a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性</a></strong>：本卡是 review 設計時要看「frame 軸」的具體 instance。寫 case-analyses 時 review 不能只看「結構是否符合 WRAP」、要看「Widen Options 是否真的並陳 hypothesis、還是塌成稻草人」。Frame 軸的覆蓋要包含 framing 結構檢查、不只是規則 check。</p>
</li>
<li>
<p><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></strong>：本卡是 #82 在「framing 層級」的具體案例。lint 規則層（字面）catch 得到「不是」「不要」等否定詞密度、但 catch 不到「整段 framing 結構是稻草人」這個行為層問題。Framing 違規屬於 #82 的「行為精煉」維度、需要 reviewer agent 跑 multi-pass 才能捕獲。</p>
</li>
<li>
<p><strong><a href="../writing-multi-pass-review/">#83 Writing multi-pass review</a></strong>：本卡是 review 第 4-5 輪該掃的「framing 維度」。前幾輪掃結構 / 術語 / 規範、framing 屬於 review 後段才能看穿的層次（因為要對全文 narrative 結構評估、不是單句檢查）。</p>
</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Widen Options 用「X 派 / Y 派 / Z 派」派系命名</td>
          <td>改成「解釋 (1) / (2) / (3)」中性編號、避免派系暗示</td>
      </tr>
      <tr>
          <td>某個選項沒有實際 prior（沒人持這論）</td>
          <td>該選項是稻草人、改寫成有實際擁護者的版本或刪掉</td>
      </tr>
      <tr>
          <td>Reality Test 連續用「A 不成立、B 不成立、C 成立」</td>
          <td>改成 evidence-based weight assessment、給每個解釋配 estimated 比例</td>
      </tr>
      <tr>
          <td>刪掉 Reality Test 後讀者能猜出哪個是正解</td>
          <td>稻草人結構、需要重新設計 Widen Options 讓選項真的並陳</td>
      </tr>
      <tr>
          <td>文章 opening 用「市場敘事 X、但 X 不重要、Y 才是」</td>
          <td>改成「正向陳述事件 + 結構性論點」、把對他人敘事的回應降為下游表象</td>
      </tr>
      <tr>
          <td>結論斷言「正確解釋是 X」「最強訊號是 Y」</td>
          <td>改成「相容度最高 / 能解釋以下 N 項觀察」evidence-based 措辭</td>
      </tr>
      <tr>
          <td>Reviewer 報告說「兩弱一強結構」「選項都很容易被打掉」</td>
          <td>框架被當修辭、不是 hypothesis 探索、改 framing pivot</td>
      </tr>
      <tr>
          <td>多個 reviewer 獨立 catch 到同一個結構問題</td>
          <td>不是個別失誤、是 systematic 陷阱、要從 framing 層改、不只 word swap</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用範圍</strong>：
<ul>
<li>用 WRAP 框架寫商業 case-analyses、市場事件拆解、產業策略分析</li>
<li>用其他類似「列選項 / 對比 / 收斂」框架寫案例（5W1H、5-Forces、JTBD 階段）</li>
<li>寫「對抗主流敘事 + 提出新解釋」類型的分析文章—這類最容易踩</li>
</ul>
</li>
<li><strong>不適用</strong>：
<ul>
<li>純技術教學（無 hypothesis space 探索、Widen Options 段落本身不存在）</li>
<li>既有 case 的事後紀錄（Outcome 已定、不需要 widen）</li>
<li>觀察筆記（明確標為個人觀察、不假裝是結構化分析）</li>
</ul>
</li>
<li><strong>邊界</strong>：「對抗稻草人」跟「列負面反例做對照」不同。AGENTS.md 原則二允許「反例段落、目的為對照」、本卡禁的是「把對抗稻草人當 narrative 主軸」。少量負面反例可保留、整段不可由稻草人結構主導。判別線：反例是否單獨成段並主導 narrative 結構—成段主導就違規、附句對照就可接受。</li>
</ul>
]]></content:encoded></item><item><title>WRAP 是寫作者的內部工具、不是文章章節結構</title><link>https://tarrragon.github.io/blog/report/wrap-as-internal-tool-not-section-structure/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/wrap-as-internal-tool-not-section-structure/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>WRAP 框架（含 Anchor Check / Step 0 / Widen Options / Reality Test / Attain Distance / Prepare to be Wrong / Tripwire）是寫作者背後做 hypothesis space 探索與認知偏誤防護的內部工具。當把這些 process 標籤暴露成文章章節結構、讀者體驗會被三個壞 effect 破壞：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>壞 effect&lt;/th>
 &lt;th>症狀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>預設讀者認知對齊&lt;/td>
 &lt;td>開頭用「X 是這套機制的末端表象」假設讀者已有 X 的認知、但「事件本身」段還沒交代&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>塞滿分析報告 meta dialogue&lt;/td>
 &lt;td>章節充斥「我們不討論什麼 / 錨點問題是什麼 / 資料充足度判斷是 X」這類寫作者內部 review 對話&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同論點重複預告&lt;/td>
 &lt;td>在開頭、Anchor Check、Step 0 三個段落各預告一次「本篇要拆什麼」、推進緩慢、讀者第三遍才開始接觸實質內容&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>修法是把 WRAP 工作內嵌進教學 narrative、章節順序服從「讀者最快理解事件結構」的教學流程、不是「WRAP 七步驟」的 process 順序。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>寫 3 篇商業 case-analyses（&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/claude-for-legal/" data-link-title="Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓" data-link-desc="用 WRAP 框架拆解基礎模型供應商進入垂直市場觸發的三層結構轉變：應用層 SaaS 毛利擠壓、新創淘汰、知識工作者判斷賭注放大">Claude for Legal&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/business/case-analyses/fde-arms-race/" data-link-title="FDE 軍備競賽：SaaS 支柱鬆動下的結構性轉變" data-link-desc="用 WRAP 框架拆解三家基礎模型供應商同時押 FDE 模式背後的 SaaS 商業前提鬆動，並判讀 FDE 是過渡狀態還是長期結構">FDE 軍備競賽&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/business/case-analyses/bufstream-acquisition/" data-link-title="CoreWeave 收購 Bufstream：整併週期下的賽道判讀與基礎設施重組" data-link-desc="用 WRAP 框架拆解 CoreWeave 買 Bufstream 揭露的串流市場整併、算力廠商對基礎設施的剛需、以及對資料工程師職涯的意涵">CoreWeave 收購 Bufstream&lt;/a>）第一版時、把 WRAP 七步驟全部當章節標題暴露給讀者：&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">## Anchor Check：要回答什麼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">## Step 0：資料充足度
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">## Widen Options：N 個解釋路徑
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">## Reality Test：用實證驗證
&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">## Attain Distance：長期影響
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">## Prepare to be Wrong：預先設計失敗回退
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">## Tripwire：何時重新評估
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">## 結論：可遷移框架&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>3-reviewer audit + Round 2 重寫後、讀者再次 feedback 指出三個具體問題：&lt;/p>
&lt;p>第一、claude-for-legal 開頭寫「『律師會被取代』是這套機制的末端表象、本篇從上游動作開始拆」—但「事件本身」段在開頭之後、讀者還不知道有「律師會被取代」這個敘事存在、開頭預設了讀者跟作者共享的 context。&lt;/p>
&lt;p>第二、Anchor Check 段寫「錨點問題聚焦在結構、而非個別公司執行力」—這是「為什麼不討論某個非問題」的 disclaim、屬於分析報告 frame、不是教學 frame。讀者根本沒問「會討論個別公司執行力嗎」、預先 disclaim 反而增加閱讀成本。&lt;/p>
&lt;p>第三、「這個動作對應用層 SaaS、新創、知識工作者三層分別造成什麼影響」這個論點在開頭、事件本身、Anchor Check 三段各出現一次—讀者讀到 Anchor Check 還沒接觸實質內容、只是被預告了三次同一件事。&lt;/p>
&lt;p>更深層的問題是：把 WRAP 從「寫作者的內部工具」當成「文章的章節結構」。WRAP 是寫作者背後 review 自己有沒有做完 hypothesis 探索的 checklist、不是讀者要走的步驟序列。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步wrap-工作在腦中或草稿跑完不暴露到讀者">第一步：WRAP 工作在腦中或草稿跑完、不暴露到讀者&lt;/h3>
&lt;p>WRAP 七步驟是寫作者完稿前要做完的 internal review：&lt;/p>
&lt;ul>
&lt;li>我有沒有做 Anchor Check（搞清楚要回答什麼）？&lt;/li>
&lt;li>我手上的 evidence 夠不夠下結論（Step 0 資料充足度）？&lt;/li>
&lt;li>我有沒有列出所有合理因果解釋（Widen Options）？&lt;/li>
&lt;li>每個解釋我都用 evidence 驗證了（Reality Test）？&lt;/li>
&lt;li>我有看 5-10 年長期影響嗎（Attain Distance）？&lt;/li>
&lt;li>我列了關鍵假設跟反證訊號嗎（Prepare to be Wrong）？&lt;/li>
&lt;li>我設了何時重新評估的 Tripwire 嗎？&lt;/li>
&lt;/ul>
&lt;p>這七題自己回答完、不寫進文章。文章是教學 deliverable、不是 review process 的 paper trail。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>WRAP 框架（含 Anchor Check / Step 0 / Widen Options / Reality Test / Attain Distance / Prepare to be Wrong / Tripwire）是寫作者背後做 hypothesis space 探索與認知偏誤防護的內部工具。當把這些 process 標籤暴露成文章章節結構、讀者體驗會被三個壞 effect 破壞：</p>
<table>
  <thead>
      <tr>
          <th>壞 effect</th>
          <th>症狀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>預設讀者認知對齊</td>
          <td>開頭用「X 是這套機制的末端表象」假設讀者已有 X 的認知、但「事件本身」段還沒交代</td>
      </tr>
      <tr>
          <td>塞滿分析報告 meta dialogue</td>
          <td>章節充斥「我們不討論什麼 / 錨點問題是什麼 / 資料充足度判斷是 X」這類寫作者內部 review 對話</td>
      </tr>
      <tr>
          <td>同論點重複預告</td>
          <td>在開頭、Anchor Check、Step 0 三個段落各預告一次「本篇要拆什麼」、推進緩慢、讀者第三遍才開始接觸實質內容</td>
      </tr>
  </tbody>
</table>
<p>修法是把 WRAP 工作內嵌進教學 narrative、章節順序服從「讀者最快理解事件結構」的教學流程、不是「WRAP 七步驟」的 process 順序。</p>
<hr>
<h2 id="情境">情境</h2>
<p>寫 3 篇商業 case-analyses（<a href="/blog/business/case-analyses/claude-for-legal/" data-link-title="Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓" data-link-desc="用 WRAP 框架拆解基礎模型供應商進入垂直市場觸發的三層結構轉變：應用層 SaaS 毛利擠壓、新創淘汰、知識工作者判斷賭注放大">Claude for Legal</a> / <a href="/blog/business/case-analyses/fde-arms-race/" data-link-title="FDE 軍備競賽：SaaS 支柱鬆動下的結構性轉變" data-link-desc="用 WRAP 框架拆解三家基礎模型供應商同時押 FDE 模式背後的 SaaS 商業前提鬆動，並判讀 FDE 是過渡狀態還是長期結構">FDE 軍備競賽</a> / <a href="/blog/business/case-analyses/bufstream-acquisition/" data-link-title="CoreWeave 收購 Bufstream：整併週期下的賽道判讀與基礎設施重組" data-link-desc="用 WRAP 框架拆解 CoreWeave 買 Bufstream 揭露的串流市場整併、算力廠商對基礎設施的剛需、以及對資料工程師職涯的意涵">CoreWeave 收購 Bufstream</a>）第一版時、把 WRAP 七步驟全部當章節標題暴露給讀者：</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">## Anchor Check：要回答什麼
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">## Step 0：資料充足度
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">## Widen Options：N 個解釋路徑
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">## Reality Test：用實證驗證
</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">## Attain Distance：長期影響
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">## Prepare to be Wrong：預先設計失敗回退
</span></span><span class="line"><span class="ln">10</span><span class="cl">## Tripwire：何時重新評估
</span></span><span class="line"><span class="ln">11</span><span class="cl">## 結論：可遷移框架</span></span></code></pre></div><p>3-reviewer audit + Round 2 重寫後、讀者再次 feedback 指出三個具體問題：</p>
<p>第一、claude-for-legal 開頭寫「『律師會被取代』是這套機制的末端表象、本篇從上游動作開始拆」—但「事件本身」段在開頭之後、讀者還不知道有「律師會被取代」這個敘事存在、開頭預設了讀者跟作者共享的 context。</p>
<p>第二、Anchor Check 段寫「錨點問題聚焦在結構、而非個別公司執行力」—這是「為什麼不討論某個非問題」的 disclaim、屬於分析報告 frame、不是教學 frame。讀者根本沒問「會討論個別公司執行力嗎」、預先 disclaim 反而增加閱讀成本。</p>
<p>第三、「這個動作對應用層 SaaS、新創、知識工作者三層分別造成什麼影響」這個論點在開頭、事件本身、Anchor Check 三段各出現一次—讀者讀到 Anchor Check 還沒接觸實質內容、只是被預告了三次同一件事。</p>
<p>更深層的問題是：把 WRAP 從「寫作者的內部工具」當成「文章的章節結構」。WRAP 是寫作者背後 review 自己有沒有做完 hypothesis 探索的 checklist、不是讀者要走的步驟序列。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步wrap-工作在腦中或草稿跑完不暴露到讀者">第一步：WRAP 工作在腦中或草稿跑完、不暴露到讀者</h3>
<p>WRAP 七步驟是寫作者完稿前要做完的 internal review：</p>
<ul>
<li>我有沒有做 Anchor Check（搞清楚要回答什麼）？</li>
<li>我手上的 evidence 夠不夠下結論（Step 0 資料充足度）？</li>
<li>我有沒有列出所有合理因果解釋（Widen Options）？</li>
<li>每個解釋我都用 evidence 驗證了（Reality Test）？</li>
<li>我有看 5-10 年長期影響嗎（Attain Distance）？</li>
<li>我列了關鍵假設跟反證訊號嗎（Prepare to be Wrong）？</li>
<li>我設了何時重新評估的 Tripwire 嗎？</li>
</ul>
<p>這七題自己回答完、不寫進文章。文章是教學 deliverable、不是 review process 的 paper trail。</p>
<h3 id="第二步章節結構服從教學流程不是-wrap-步驟順序">第二步：章節結構服從教學流程、不是 WRAP 步驟順序</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">[開頭 1 段]                 直接描述事件 + 一句帶到本篇拆解什麼
</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">## 為什麼 X（教學段）         把 Widen Options + Reality Test 內嵌進教學 narrative
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                            含並陳因果解釋（每個有 prior + prediction）+ evidence 配重
</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">                            按層 / 維度 / 時間軸組織、不按 WRAP 步驟
</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">## 長期影響與機會成本          Attain Distance 內容、移除 process 標籤
</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">## 預警訊號                   Prepare to be Wrong + Tripwire 合併
</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></span><span class="line"><span class="ln">18</span><span class="cl">## 可遷移的判讀框架           結論段、給讀者帶走的工具</span></span></code></pre></div><h3 id="第三步開頭不對抗他人敘事不預設讀者認知">第三步：開頭不對抗他人敘事、不預設讀者認知</h3>
<p>開頭只做兩件事：</p>
<ul>
<li>描述事件（讀者進入 context）</li>
<li>一句話帶到本篇要拆什麼（讀者知道接下來會讀到什麼）</li>
</ul>
<p>不做這些事：</p>
<ul>
<li>「市場敘事是 X、但 X 不重要、Y 才是」（contrarian framing、見 <a href="../wrap-widen-options-strawman-risk/">#140</a>）</li>
<li>「X 是這套機制的末端表象、本篇從上游動作開始拆」（預設讀者已有 X 的認知）</li>
<li>「我們不討論個別公司執行力」（分析報告 frame、不是教學 frame）</li>
</ul>
<p>對其他敘事的回應、放到「事件本身」段、有 context 後才提：「公開討論集中在 X—這是這個動作的下游表象、本篇焦點在觸發它的上游機制」。讀者讀完「事件本身」段、才有 context 理解 X 是什麼、預設讀者認知的問題自然消失。</p>
<h3 id="第四步完稿時跑process-metadata-掃描">第四步：完稿時跑「process metadata 掃描」</h3>
<p>寫完後、grep 文章章節標題、檢查有沒有殘留：</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">grep -E <span class="s2">&#34;^## (Anchor Check|Step 0|Widen Options|Reality Test|Attain Distance|Prepare to be Wrong|Tripwire)&#34;</span> *.md</span></span></code></pre></div><p>任何命中、都是 WRAP process metadata 暴露給讀者。改成教學標題：</p>
<table>
  <thead>
      <tr>
          <th>WRAP 標題</th>
          <th>教學標題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anchor Check</td>
          <td>（刪除、放開頭段或事件本身段）</td>
      </tr>
      <tr>
          <td>Step 0 資料充足度</td>
          <td>（刪除、放在「為什麼 X」段內）</td>
      </tr>
      <tr>
          <td>Widen Options</td>
          <td>為什麼供應商選擇 X / 為什麼買方出手</td>
      </tr>
      <tr>
          <td>Reality Test</td>
          <td>（同上段、不單獨成段）</td>
      </tr>
      <tr>
          <td>Attain Distance</td>
          <td>長期影響與機會成本</td>
      </tr>
      <tr>
          <td>Prepare to be Wrong</td>
          <td>預警訊號：何時重新評估這個分析</td>
      </tr>
      <tr>
          <td>Tripwire</td>
          <td>（同上段、用表格列訊號）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="讀者預期被預設認知門檻不對齊">讀者預期被預設、認知門檻不對齊</h3>
<p>開頭「X 是末端表象、本篇拆上游」要求讀者已經知道 X 是什麼。如果 X 在後文才介紹、讀者在開頭就被 confused、之後讀「事件本身」段才搞懂、回去重讀開頭—閱讀成本翻倍。</p>
<h3 id="register-從教學滑成分析報告">Register 從教學滑成分析報告</h3>
<p>「Anchor Check」「Step 0 資料充足度」「Reality Test」這類詞屬於 analyst 內部對 own work 的 review 對話、不是給讀者看的章節標題。把這些暴露給讀者、整篇 register 從「教學知識庫」滑成「給同行看的分析報告」、讀者輪廓變成「已經懂 WRAP 框架的分析師」而非「想學商業分析的工程師」。</p>
<h3 id="同論點重複預告推進緩慢">同論點重複預告、推進緩慢</h3>
<p>WRAP 七步驟本來就有「先講要回答什麼、再講資料、再講選項、再驗證」的內在重複—每一步都會提到核心論點一次。如果七個步驟全部變章節、核心論點會在前三段被預告三次（開頭、Anchor Check、Step 0 都會講「本篇要拆什麼」）、讀者讀到 Reality Test 才接觸實質內容。</p>
<h3 id="wrap-框架的價值被當成修辭裝飾">WRAP 框架的價值被當成修辭裝飾</h3>
<p>WRAP 的真正價值是讓寫作者做 hypothesis 探索、防認知偏誤。當 WRAP 變章節結構、讀者跟寫作者都會把它當成「應該照走的 process」、原本的 hypothesis 探索變成「填表」、防認知偏誤的功能失效。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>
<p><strong><a href="../wrap-widen-options-strawman-risk/">#140 WRAP Widen Options 容易塌成稻草人 framing</a></strong>：本卡是 #140 的上位原則。#140 處理 Widen Options 段落的內容違規（兩弱一強稻草人）、本卡處理 WRAP 整套被當章節結構暴露的 surface 違規。兩卡互補—改 Widen Options 內容不夠、還要改 surface presentation。</p>
</li>
<li>
<p><strong><a href="../collapse-is-implicit-default/">#125 Collapse 是隱形預設</a></strong>：本卡是 #125 在「寫作 process 透明度」surface 的具體 instance。Process metadata 暴露給讀者是「省去設計教學流程的便利選擇」的後果—不思考章節順序、直接把 WRAP 步驟當章節是最便利、但 collapse 掉了「為讀者設計閱讀順序」這個維度。</p>
</li>
<li>
<p><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></strong>：本卡是 #82 在「process metadata 暴露」維度的具體案例。lint 規則層 catch 不到「章節標題是 Anchor Check 還是『為什麼 X』」、要靠 reviewer / 讀者 feedback 才能發現—屬於 #82 的「行為精煉」維度。</p>
</li>
<li>
<p><strong><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></strong>：本卡擴 #97 的 metadata surface 概念。#97 處理 title / description / heading 是讀者入口的 metadata；本卡指出 <em>章節結構本身</em> 也是 metadata surface—章節標題傳達「文章是什麼類型」、process 標題傳達「這是分析報告」、教學標題傳達「這是教學文章」。</p>
</li>
<li>
<p><strong><a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性</a></strong>：本卡是 review 設計時要看的「surface 軸」的具體 instance。Review 不能只看「內容是否正確」、要看「章節結構傳達的 register 是否跟目標讀者對齊」。</p>
</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>章節標題出現「Anchor Check / Step 0 / Widen Options / Reality Test」</td>
          <td>改成教學標題、把 WRAP 內容融進教學段</td>
      </tr>
      <tr>
          <td>開頭預設讀者已有某個敘事的認知（例如「X 是末端表象」）</td>
          <td>把對該敘事的回應移到「事件本身」段、開頭只描述事件 + 帶到本篇主題</td>
      </tr>
      <tr>
          <td>文章有「我們不討論 X」這類分析報告 disclaim</td>
          <td>刪、教學文章不需要預先排除某些議題、讀者沒問</td>
      </tr>
      <tr>
          <td>同一論點在開頭、Anchor Check、Step 0 各預告一次</td>
          <td>改成只在開頭預告一次、後續直接推進</td>
      </tr>
      <tr>
          <td>章節順序嚴格按 WRAP 步驟</td>
          <td>改成按教學流程：開頭 → 事件 → 為什麼 X → 結構性機制 → 長期 → 預警 → 框架</td>
      </tr>
      <tr>
          <td>讀者反饋「文章像分析報告、不像教學」</td>
          <td>Register 漂移、check 是不是 WRAP process metadata 暴露</td>
      </tr>
      <tr>
          <td>Reviewer 報告「文章預設讀者已有某種認知」</td>
          <td>開頭結構有問題、修法見本卡「開頭不對抗他人敘事、不預設讀者認知」段</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用範圍</strong>：
<ul>
<li>用 WRAP 框架寫商業 case-analyses、市場事件拆解、產業策略分析</li>
<li>用其他「先列 process、再走 process」框架寫教學文章（5W1H、5-Forces 分步、JTBD 階段、case method 步驟）</li>
<li>任何「寫作者內部 review tool」跟「讀者教學 narrative」混淆的情境</li>
</ul>
</li>
<li><strong>不適用</strong>：
<ul>
<li>給同行看的分析報告（analyst-to-analyst、process metadata 是正當的）</li>
<li>學術論文（IMRaD 結構有正當性、process 標題是學術慣例）</li>
<li>純技術 reference 文件（process metadata 反而幫助快速定位）</li>
</ul>
</li>
<li><strong>邊界</strong>：本卡禁的是「把 WRAP 整套步驟當文章章節結構」、不是禁所有 process 詞彙。在合適的位置提一句「以下用 WRAP 框架背後的 hypothesis 探索方法拆」是 OK 的、整篇用 WRAP 步驟當章節就違規。判別線：章節標題是描述「讀者會學到什麼」（教學）還是「作者在做什麼分析步驟」（process）。</li>
</ul>
]]></content:encoded></item><item><title>文章主體要對齊標題承諾、WRAP 內部分析不該喧賓奪主</title><link>https://tarrragon.github.io/blog/report/article-body-must-align-with-title-commitment/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/article-body-must-align-with-title-commitment/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>文章標題對讀者做了承諾、文章主體必須對齊這個承諾。WRAP 內部分析（Widen Options + Reality Test 含 prior 引用 + evidence weight）即使方法論做得好、若不是標題承諾的內容、就不該佔文章主體。&lt;/p>
&lt;p>這跟 &lt;a href="../wrap-as-internal-tool-not-section-structure/">#141 WRAP 是寫作者的內部工具、不是文章章節結構&lt;/a> 是兩個不同層級的議題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>&lt;a href="../wrap-as-internal-tool-not-section-structure/">#141&lt;/a>&lt;/th>
 &lt;th>本卡 (#142)&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>處理層級&lt;/td>
 &lt;td>章節標題（surface）&lt;/td>
 &lt;td>章節內容（scope）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>違規症狀&lt;/td>
 &lt;td>Process metadata 標題（「Widen Options」「Reality Test」）&lt;/td>
 &lt;td>即使標題改了、內容仍是 WRAP 內部分析、偏離標題承諾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修法&lt;/td>
 &lt;td>改章節標題為教學風格&lt;/td>
 &lt;td>縮減 WRAP 內部分析篇幅、聚焦標題承諾的內容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>附帶副作用&lt;/td>
 &lt;td>預設讀者認知、分析報告 meta dialogue、重複預告&lt;/td>
 &lt;td>Source citation hallucination 風險、解釋順序錯位（source 在前、解釋在後）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩卡互補—改了章節標題還不夠、章節內容也要對齊標題承諾。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>3 篇 case-analyses 經過 &lt;a href="../wrap-as-internal-tool-not-section-structure/">#141&lt;/a> 的 Round 3 重寫、章節標題已從 WRAP process metadata 改成教學風格（「為什麼供應商選擇 enterprise 包裝」取代「Widen Options」）。但讀者再次 feedback 指出更深的問題：&lt;/p>
&lt;p>第一、&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/claude-for-legal/" data-link-title="Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓" data-link-desc="用 WRAP 框架拆解基礎模型供應商進入垂直市場觸發的三層結構轉變：應用層 SaaS 毛利擠壓、新創淘汰、知識工作者判斷賭注放大">Claude for Legal 之後&lt;/a> 的標題承諾「應用層、新創、知識工作者的三層擠壓」、但「供應商為什麼選擇 enterprise 包裝」段佔了文章 30%+ 篇幅—讀者拿到的內容跟標題承諾不匹配。&lt;/p>
&lt;p>第二、那一段內容引用「a16z、Sequoia 公開報告跟 Anthropic 投資人 deck 都強調 enterprise ARR」這類 source—但這些引用沒具體出處（哪份報告、哪一頁、哪一段）、有 hallucination 風險。為了支撐 WRAP Widen Options 的 prior 而引入沒實際出處的 source、是 fidelity 漏洞。&lt;/p>
&lt;p>第三、解釋順序錯位—寫成「a16z / Sequoia 等公開報告強調 ARR、背後邏輯是 X」、把 source 放前面、解釋放後面、違反 AGENTS.md 原則一「核心原則先行」。&lt;/p>
&lt;p>更深層問題：即使章節標題改成教學風格、WRAP 內部分析（含 prior 引用）的內容仍然喧賓奪主、偏離標題承諾。3 篇都踩同樣的 pattern：&lt;/p>
&lt;ul>
&lt;li>claude-for-legal 標題「三層擠壓」、原版「供應商為什麼選擇 enterprise 包裝」段佔大量篇幅、含 hallucinated source&lt;/li>
&lt;li>fde-arms-race 標題「SaaS 三支柱鬆動」、原版「三家為什麼同步押 FDE」段佔了三支柱主體的空間&lt;/li>
&lt;li>bufstream 標題「整併週期 + 基礎設施重組」、原版「Buf 為什麼賣」「CoreWeave 為什麼買」兩段 WRAP 分析佔主體&lt;/li>
&lt;/ul>
&lt;p>Round 4 重寫後、3 篇都移除「為什麼 X」獨立段、把核心動機塞進「事件本身」一兩句 + cross-link 到處理該動機的對應文章、文章主體留給標題承諾的內容。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步寫稿前明確列出標題承諾什麼">第一步：寫稿前明確列出標題承諾什麼&lt;/h3>
&lt;p>標題是讀者跟文章之間的合約。寫稿前用一句話寫下「這篇標題承諾讀者拿到什麼」：&lt;/p>
&lt;ul>
&lt;li>「Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓」→ 承諾三層擠壓的拆解&lt;/li>
&lt;li>「FDE 軍備競賽：SaaS 三支柱鬆動下的結構性轉變」→ 承諾三支柱怎麼鬆動的機制&lt;/li>
&lt;li>「CoreWeave 收購 Bufstream：整併週期下的賽道判讀與基礎設施重組」→ 承諾整併週期判讀 + 基礎設施重組分析&lt;/li>
&lt;/ul>
&lt;p>承諾寫下後、後續每個章節都對齊問：「這段是不是在履行承諾？」&lt;/p>
&lt;h3 id="第二步跑完-wrap-內部分析區分主結論跟分析過程">第二步：跑完 WRAP 內部分析、區分「主結論」跟「分析過程」&lt;/h3>
&lt;p>WRAP 七步驟在寫作者腦中跑完—Anchor Check / Step 0 / Widen Options / Reality Test / Attain Distance / Prepare to be Wrong / Tripwire 都要做。但跑完後、分兩類東西：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>主結論&lt;/strong>：可以放文章主體、是讀者要拿走的判讀&lt;/li>
&lt;li>&lt;strong>分析過程&lt;/strong>：（Widen Options 的多個解釋、Reality Test 的逐一驗證、Prior 的 source 引用）留在腦中或寫作筆記、不放進文章&lt;/li>
&lt;/ul>
&lt;p>判別線是「這段內容是不是標題承諾的一部分」。承諾「三層擠壓」、文章主體就是三層擠壓；供應商動機是 prelude / context、塞進「事件本身」一兩句帶過、不獨立成段做完整 WRAP 分析。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>文章標題對讀者做了承諾、文章主體必須對齊這個承諾。WRAP 內部分析（Widen Options + Reality Test 含 prior 引用 + evidence weight）即使方法論做得好、若不是標題承諾的內容、就不該佔文章主體。</p>
<p>這跟 <a href="../wrap-as-internal-tool-not-section-structure/">#141 WRAP 是寫作者的內部工具、不是文章章節結構</a> 是兩個不同層級的議題：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th><a href="../wrap-as-internal-tool-not-section-structure/">#141</a></th>
          <th>本卡 (#142)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>處理層級</td>
          <td>章節標題（surface）</td>
          <td>章節內容（scope）</td>
      </tr>
      <tr>
          <td>違規症狀</td>
          <td>Process metadata 標題（「Widen Options」「Reality Test」）</td>
          <td>即使標題改了、內容仍是 WRAP 內部分析、偏離標題承諾</td>
      </tr>
      <tr>
          <td>修法</td>
          <td>改章節標題為教學風格</td>
          <td>縮減 WRAP 內部分析篇幅、聚焦標題承諾的內容</td>
      </tr>
      <tr>
          <td>附帶副作用</td>
          <td>預設讀者認知、分析報告 meta dialogue、重複預告</td>
          <td>Source citation hallucination 風險、解釋順序錯位（source 在前、解釋在後）</td>
      </tr>
  </tbody>
</table>
<p>兩卡互補—改了章節標題還不夠、章節內容也要對齊標題承諾。</p>
<hr>
<h2 id="情境">情境</h2>
<p>3 篇 case-analyses 經過 <a href="../wrap-as-internal-tool-not-section-structure/">#141</a> 的 Round 3 重寫、章節標題已從 WRAP process metadata 改成教學風格（「為什麼供應商選擇 enterprise 包裝」取代「Widen Options」）。但讀者再次 feedback 指出更深的問題：</p>
<p>第一、<a href="/blog/business/case-analyses/claude-for-legal/" data-link-title="Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓" data-link-desc="用 WRAP 框架拆解基礎模型供應商進入垂直市場觸發的三層結構轉變：應用層 SaaS 毛利擠壓、新創淘汰、知識工作者判斷賭注放大">Claude for Legal 之後</a> 的標題承諾「應用層、新創、知識工作者的三層擠壓」、但「供應商為什麼選擇 enterprise 包裝」段佔了文章 30%+ 篇幅—讀者拿到的內容跟標題承諾不匹配。</p>
<p>第二、那一段內容引用「a16z、Sequoia 公開報告跟 Anthropic 投資人 deck 都強調 enterprise ARR」這類 source—但這些引用沒具體出處（哪份報告、哪一頁、哪一段）、有 hallucination 風險。為了支撐 WRAP Widen Options 的 prior 而引入沒實際出處的 source、是 fidelity 漏洞。</p>
<p>第三、解釋順序錯位—寫成「a16z / Sequoia 等公開報告強調 ARR、背後邏輯是 X」、把 source 放前面、解釋放後面、違反 AGENTS.md 原則一「核心原則先行」。</p>
<p>更深層問題：即使章節標題改成教學風格、WRAP 內部分析（含 prior 引用）的內容仍然喧賓奪主、偏離標題承諾。3 篇都踩同樣的 pattern：</p>
<ul>
<li>claude-for-legal 標題「三層擠壓」、原版「供應商為什麼選擇 enterprise 包裝」段佔大量篇幅、含 hallucinated source</li>
<li>fde-arms-race 標題「SaaS 三支柱鬆動」、原版「三家為什麼同步押 FDE」段佔了三支柱主體的空間</li>
<li>bufstream 標題「整併週期 + 基礎設施重組」、原版「Buf 為什麼賣」「CoreWeave 為什麼買」兩段 WRAP 分析佔主體</li>
</ul>
<p>Round 4 重寫後、3 篇都移除「為什麼 X」獨立段、把核心動機塞進「事件本身」一兩句 + cross-link 到處理該動機的對應文章、文章主體留給標題承諾的內容。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步寫稿前明確列出標題承諾什麼">第一步：寫稿前明確列出標題承諾什麼</h3>
<p>標題是讀者跟文章之間的合約。寫稿前用一句話寫下「這篇標題承諾讀者拿到什麼」：</p>
<ul>
<li>「Claude for Legal 之後：應用層、新創、知識工作者的三層擠壓」→ 承諾三層擠壓的拆解</li>
<li>「FDE 軍備競賽：SaaS 三支柱鬆動下的結構性轉變」→ 承諾三支柱怎麼鬆動的機制</li>
<li>「CoreWeave 收購 Bufstream：整併週期下的賽道判讀與基礎設施重組」→ 承諾整併週期判讀 + 基礎設施重組分析</li>
</ul>
<p>承諾寫下後、後續每個章節都對齊問：「這段是不是在履行承諾？」</p>
<h3 id="第二步跑完-wrap-內部分析區分主結論跟分析過程">第二步：跑完 WRAP 內部分析、區分「主結論」跟「分析過程」</h3>
<p>WRAP 七步驟在寫作者腦中跑完—Anchor Check / Step 0 / Widen Options / Reality Test / Attain Distance / Prepare to be Wrong / Tripwire 都要做。但跑完後、分兩類東西：</p>
<ul>
<li><strong>主結論</strong>：可以放文章主體、是讀者要拿走的判讀</li>
<li><strong>分析過程</strong>：（Widen Options 的多個解釋、Reality Test 的逐一驗證、Prior 的 source 引用）留在腦中或寫作筆記、不放進文章</li>
</ul>
<p>判別線是「這段內容是不是標題承諾的一部分」。承諾「三層擠壓」、文章主體就是三層擠壓；供應商動機是 prelude / context、塞進「事件本身」一兩句帶過、不獨立成段做完整 WRAP 分析。</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></code></pre></div><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">- 事件本身 (10%) — 提供 context、合理
</span></span><span class="line"><span class="ln">2</span><span class="cl">- 供應商為什麼選擇 enterprise 包裝 (30%) — 不在標題承諾、過度
</span></span><span class="line"><span class="ln">3</span><span class="cl">- 第一層擠壓 (15%) — 對齊承諾
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 第二層擠壓 (15%) — 對齊承諾
</span></span><span class="line"><span class="ln">5</span><span class="cl">- 第三層擠壓 (15%) — 對齊承諾
</span></span><span class="line"><span class="ln">6</span><span class="cl">- 長期影響 (10%) — 對齊承諾
</span></span><span class="line"><span class="ln">7</span><span class="cl">- 預警訊號 + 框架 (5%) — 對齊承諾</span></span></code></pre></div><p>「不在標題承諾」的段落佔 &gt; 20% 就要重寫—把該段縮成一兩句塞進「事件本身」、cross-link 到處理該議題的對應文章、不獨立展開。</p>
<h3 id="第四步source-citation-必須真實可驗證">第四步：Source citation 必須真實可驗證</h3>
<p>引用 source 時遵守三條規則：</p>
<ol>
<li><strong>能 verify 才寫</strong>：引用「a16z 報告」「Sequoia 分析」前、確認你看過該報告、能給具體標題或連結。不能就改成 hedged claim（「業界普遍觀察」「分析師多次點過」）。</li>
<li><strong>解釋在前、source 在後</strong>：「API 利潤太薄需要長合約對沖（這個論點 a16z 多次公開分析）」、不是「a16z 公開分析 API 利潤太薄、所以需要長合約對沖」。核心原則先行—讀者先吸收解釋、再判斷 source 可信度。</li>
<li><strong>不確定就刪掉 source 引用</strong>：寫到「a16z、Sequoia、Andreessen」這種列舉時要問「我真的能列出三家都講過這個論點嗎？」答案是「不確定」就改成「業界普遍觀察」、不列 specific 名字。</li>
</ol>
<h3 id="第五步wrap-內部分析的次要結論的處理">第五步：WRAP 內部分析的次要結論的處理</h3>
<p>WRAP 內部分析跑完後、會產出「主結論 + 次要結論」。主結論放標題承諾的主體、次要結論的處理：</p>
<table>
  <thead>
      <tr>
          <th>次要結論類型</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跟其他文章主題重疊</td>
          <td>Cross-link 過去、不展開（claude-for-legal 把供應商動機 cross-link 到 fde-arms-race）</td>
      </tr>
      <tr>
          <td>提供事件 context</td>
          <td>塞進「事件本身」段一兩句、不獨立成段</td>
      </tr>
      <tr>
          <td>完全偏離本篇主題</td>
          <td>留在寫作筆記、可能變成另一篇文章</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="讀者拿到的內容跟標題承諾不匹配">讀者拿到的內容跟標題承諾不匹配</h3>
<p>標題承諾「三層擠壓」、文章主體 30% 在講「供應商為什麼選擇 enterprise 包裝」、讀者拿走的判讀工具偏離標題暗示的方向。這比章節標題違規更隱蔽—章節標題違規讀者一眼看穿、內容偏離標題要讀完才發現、傷害更深。</p>
<h3 id="hallucinated-source-citation-的-fidelity-漏洞">Hallucinated source citation 的 fidelity 漏洞</h3>
<p>WRAP Widen Options 需要 prior 支撐（「誰持這論」）、寫作者為了證明 prior 存在、容易引用「a16z、Sequoia、Andreessen」這類沒具體出處的 source。讀者 trust 在 source citation 上、hallucinated source 一旦被識破、整篇文章的 fidelity 崩。</p>
<h3 id="解釋順序錯位違反核心原則先行">解釋順序錯位、違反核心原則先行</h3>
<p>當寫作者重心在「我有 source 支撐」、會把 source 放前面（「a16z 公開報告強調 X」）、解釋放後面。違反 AGENTS.md 原則一「核心原則先行」—讀者要的是解釋本身、source 是 attribution、不是 lead。</p>
<h3 id="wrap-內部分析方法論價值反而被稀釋">WRAP 內部分析方法論價值反而被稀釋</h3>
<p>WRAP 是寫作者的 hypothesis 探索工具、價值在「強制做完整探索、防認知偏誤」。當分析過程被搬上文章主體、讀者把它當成「文章內容」吸收、原本的探索工具退化成「對讀者展示我做了多少 hypothesis 探索」的修辭。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>
<p><strong><a href="../wrap-as-internal-tool-not-section-structure/">#141 WRAP 是寫作者的內部工具、不是文章章節結構</a></strong>：sibling 卡。#141 處理章節標題 surface 違規（process metadata 暴露）、本卡處理章節內容 scope 違規（WRAP 內部分析喧賓奪主）。兩卡互補—改章節標題不夠、還要改章節內容比重。</p>
</li>
<li>
<p><strong><a href="../wrap-widen-options-strawman-risk/">#140 WRAP Widen Options 容易塌成稻草人 framing</a></strong>：cousin 卡。#140 處理 Widen Options 段落內部的稻草人結構、本卡處理 Widen Options 段落「該不該存在」的更上位議題。如果 Widen Options 跟標題承諾不對齊、根本不該獨立成段、稻草人問題自然消失。</p>
</li>
<li>
<p><strong><a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證</a></strong>：本卡是 #131 在「標題承諾兌現」維度的具體 instance。讀者旅程的起點是標題暗示、終點是讀完文章能做什麼。標題承諾不兌現、讀者旅程斷在中段、完成感跟可遷移工具都失效。</p>
</li>
<li>
<p><strong><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></strong>：本卡擴 #97 的 metadata surface 概念。標題本身是 metadata surface—它對讀者承諾文章主體是什麼。Review 不能只看內文是否正確、要看「內文跟標題承諾是否對齊」。</p>
</li>
<li>
<p><strong><a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性</a></strong>：本卡是 review 設計時要看的「scope 軸 + 標題對齊軸」的具體 instance。Review 不只看 frame / instance / surface、還要看「內容範圍跟標題承諾是否對齊」、是 scope 軸的延伸應用。</p>
</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>文章某段佔 &gt; 20% 篇幅、但不在標題暗示的主題範圍</td>
          <td>縮成一兩句塞進事件本身、cross-link 到處理該議題的對應文章</td>
      </tr>
      <tr>
          <td>Widen Options / Reality Test 內容獨立成段</td>
          <td>改成內嵌進「事件本身」段一兩句、不展開完整分析</td>
      </tr>
      <tr>
          <td>Source citation 列舉「a16z、Sequoia、Andreessen」這類沒具體出處的 prior</td>
          <td>Verify 不到就改 hedged claim（「業界普遍觀察」）、不列 specific 名字</td>
      </tr>
      <tr>
          <td>段落寫成「source 公開 X、所以 X 成立」</td>
          <td>順序錯、改成「解釋本身 + 附加 source attribution」</td>
      </tr>
      <tr>
          <td>完稿後讀者反饋「文章寫了很多東西、但跟標題主題不一致」</td>
          <td>標題對齊測試失敗、要把不在標題承諾範圍的段落瘦身或移除</td>
      </tr>
      <tr>
          <td>Reviewer 報告「文章主體 30% 在講次要議題」</td>
          <td>Scope mismatch、跑標題對齊測試 + 重寫</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用範圍</strong>：
<ul>
<li>用 WRAP 框架寫商業 case-analyses、市場事件拆解、產業策略分析</li>
<li>任何「標題承諾 vs 文章主體 scope」可能不對齊的情境（深度教學文章、技術 deep-dive、產業分析）</li>
<li>文章標題明確指涉特定主題、但寫作過程容易被相關但非標題承諾的分析吸引展開</li>
</ul>
</li>
<li><strong>不適用</strong>：
<ul>
<li>探索式 essay（標題本來就模糊、scope 由內文展開）</li>
<li>短篇 commentary（內容篇幅不夠展開 scope mismatch）</li>
<li>純技術 reference（標題承諾的是「查得到」、不是「主題聚焦」）</li>
</ul>
</li>
<li><strong>邊界</strong>：本卡禁的是「WRAP 內部分析喧賓奪主」、不是禁所有 WRAP 內部分析出現在文章。標題承諾本身就是「拆解 X 動機」的文章（例如 fde-arms-race 主題就是供應商為什麼押 FDE）、那 WRAP 內部分析才是文章主體、屬於正當對齊。判別線：標題承諾的主題跟 WRAP 分析的對象是否一致。一致就展開、不一致就壓縮成 cross-link。</li>
</ul>
]]></content:encoded></item><item><title>外部分析文章要先拆成事實、作者判讀、本文推導</title><link>https://tarrragon.github.io/blog/report/external-analysis-source-layering/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/external-analysis-source-layering/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>外部分析文章是寫作材料、不是可直接搬進教學文章的事實層。把分析師文章、VC essay、產業評論改寫成本 blog 的商業分析時、先把材料拆成三層：可驗證事實、原作者判讀、本文推導。&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>標成「某類分析觀點」、只當 hypothesis&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;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>&lt;code>content/business/&lt;/code> 的原始出發點是輸入其他分析師文章、再讓 AI 轉換成本 blog 要的風格。第一版 business case-analyses 雖然已經用 WRAP 拆事件、但 commit 演變顯示三個容易混在一起的材料來源：&lt;/p>
&lt;p>第一、公開事件本身，例如 Anthropic 推出 Claude for Legal、Snowflake / Databricks / MotherDuck 同期推出 FDE、CoreWeave 收購 Bufstream。這些是可驗證事實。&lt;/p>
&lt;p>第二、外部分析師對事件的判讀，例如「這是 enterprise ARR 驅動」「這是基礎設施垂直整合」「這是 SaaS 三支柱被鬆動」。這些是原作者或市場的解釋，不是事件本身。&lt;/p>
&lt;p>第三、本文要教讀者帶走的框架，例如「三層擠壓」「資料平台前端化」「整併週期下的賽道判讀」。這些是本文推導。&lt;/p>
&lt;p>Round 4 的 &lt;code>#142&lt;/code> 已經處理「WRAP 內部分析喧賓奪主」；本卡補更上游的 source 問題：在開始寫正文前、先知道每句材料屬於哪一層。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步來源進稿前先做三欄標註">第一步：來源進稿前先做三欄標註&lt;/h3>
&lt;p>把外部文章或 AI 轉寫草稿中的句子標成三欄：&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>這件事有沒有公開來源可以驗證？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>原作者判讀&lt;/td>
 &lt;td>這是不是某位作者或某類市場敘事的解釋？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本文推導&lt;/td>
 &lt;td>這是不是本文從多個材料合成出來的教學框架？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>同一句若同時包含兩層、拆成兩句。例：「CoreWeave 收購 Bufstream，代表算力廠商開始垂直整合 data pipeline」應拆成：&lt;/p>
&lt;ol>
&lt;li>CoreWeave 收購 Bufstream。這是事實。&lt;/li>
&lt;li>算力廠商往 data pipeline 延伸。這是本文判讀。&lt;/li>
&lt;/ol>
&lt;h3 id="第二步事實進事件段判讀進-hypothesis推導進主體">第二步：事實進事件段、判讀進 hypothesis、推導進主體&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;/td>
 &lt;td>開頭與「事件本身」段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>原作者判讀&lt;/td>
 &lt;td>Widen Options 的 prior 或背景敘事&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本文推導&lt;/td>
 &lt;td>文章主體、長期影響、預警訊號、框架&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>外部分析師的判讀可以幫助 widen hypothesis space，但它不該直接變成本 blog 的正文結論。正文結論要由本文的 evidence weight 與可遷移框架承擔。&lt;/p>
&lt;h3 id="第三步合成-frame-要標成本文推導">第三步：合成 frame 要標成本文推導&lt;/h3>
&lt;p>當文章把多個來源合成成一個框架時、要讓讀者知道這是本文的教學合成。寫法可以是：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">本文把這個變化整理成三層：應用層 SaaS、新創供應商、知識工作者。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這比「市場正在發生三層擠壓」精準，因為前者承認這是本文整理出的框架，後者容易讓讀者誤以為三層分類是外部事件本身。&lt;/p>
&lt;h3 id="第四步引用來源時先寫概念再放-attribution">第四步：引用來源時先寫概念、再放 attribution&lt;/h3>
&lt;p>來源 attribution 是可回溯支撐，不是段落主詞。先寫本文要教的概念，再補來源角色：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">API 型產品若毛利薄、供應商會傾向用 enterprise contract 對沖收入波動。這個判讀可作為檢查供應商動機的 prior，不能直接當作本篇三層擠壓的主體。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不要寫成「某某報告說 X，所以 X 是結論」。這會把來源權威放在概念之前，也讓讀者無法分辨 source claim 與本文推導。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="分析師-frame-被誤當事實">分析師 frame 被誤當事實&lt;/h3>
&lt;p>外部文章的分類與比喻通常是作者的 frame。直接搬進正文，讀者會以為那是事件本身揭露的事實。後續若讀者回查來源找不到「三層擠壓」或「SaaS 三支柱鬆動」這些詞，會降低文章可信度。&lt;/p>
&lt;h3 id="ai-改寫會放大-attribution-漂移">AI 改寫會放大 attribution 漂移&lt;/h3>
&lt;p>AI 轉寫常把「某作者認為」壓縮成「這代表」。這個壓縮讓 claim 從觀點層滑到事實層。若沒有 source layering，改寫後的句子會更順、但 fidelity 更差。&lt;/p>
&lt;h3 id="本文自己的貢獻變模糊">本文自己的貢獻變模糊&lt;/h3>
&lt;p>教學型商業分析的價值是把事件整理成讀者可遷移的框架。若原作者判讀與本文推導混在一起，讀者看不出本文到底新增了什麼，只會覺得是另一篇摘要。&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>&lt;a href="../fact-vs-derive-citation-layering/">#116 引用案例要分觀察層 / 判讀層&lt;/a>&lt;/td>
 &lt;td>#116 處理 case 引用，本卡把同一條分層原則套到外部分析文章 source&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../cross-case-synthesized-frame-must-be-labeled/">#117 跨多個 case 合成的 frame 必須標為章節合成&lt;/a>&lt;/td>
 &lt;td>本卡是 #117 在 analyst source 的對應版本：跨來源合成 frame 要標成本文推導&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../article-body-must-align-with-title-commitment/">#142 文章主體要對齊標題承諾&lt;/a>&lt;/td>
 &lt;td>Source layering 是 #142 的前置條件；先知道哪些是背景 prior，才知道哪些不該佔主體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../wrap-widen-options-strawman-risk/">#140 WRAP Widen Options 容易塌成稻草人 framing&lt;/a>&lt;/td>
 &lt;td>原作者判讀可作為 Widen Options prior，但不能被設成待打倒的稻草人或預設正解&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍&lt;/a>&lt;/td>
 &lt;td>Title / description 若把本文推導寫成事實，也會造成來源層漂移；metadata 也要跑 source layering&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>句子寫「這代表 X」、但 X 其實是分析師解釋&lt;/td>
 &lt;td>改成「一種判讀是 X」或標成本文推導&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>外部文章的比喻或分類被直接放進本篇標題&lt;/td>
 &lt;td>確認這是原作者 frame 還是本文 frame&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AI 改寫後少了「某作者認為」「市場敘事」等 attribution&lt;/td>
 &lt;td>回原文補回 source layer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>引用來源只寫機構名、沒有具體文章或可驗證出處&lt;/td>
 &lt;td>刪掉具名 source，改成一般 prior 或重新查證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者問「這是事件事實、原文觀點、還是你自己的推導」&lt;/td>
 &lt;td>Source layering 失敗，重寫段落主詞與 attribution&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：外部分析文章進入教學寫作前、先拆成事實、作者判讀、本文推導。三層分清楚，文章才有可回溯性，也才看得出本文真正教了什麼。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>外部分析文章是寫作材料、不是可直接搬進教學文章的事實層。把分析師文章、VC essay、產業評論改寫成本 blog 的商業分析時、先把材料拆成三層：可驗證事實、原作者判讀、本文推導。</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>標成「某類分析觀點」、只當 hypothesis</td>
      </tr>
      <tr>
          <td>本文推導</td>
          <td>本文從多個來源合成的判斷框架</td>
          <td>明確寫成「本文判讀」或「可遷移框架」</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「這句話如果拿掉原作者名字、還能被當成可驗證事實嗎？」不能、就不可寫成事實句。</p>
<hr>
<h2 id="情境">情境</h2>
<p><code>content/business/</code> 的原始出發點是輸入其他分析師文章、再讓 AI 轉換成本 blog 要的風格。第一版 business case-analyses 雖然已經用 WRAP 拆事件、但 commit 演變顯示三個容易混在一起的材料來源：</p>
<p>第一、公開事件本身，例如 Anthropic 推出 Claude for Legal、Snowflake / Databricks / MotherDuck 同期推出 FDE、CoreWeave 收購 Bufstream。這些是可驗證事實。</p>
<p>第二、外部分析師對事件的判讀，例如「這是 enterprise ARR 驅動」「這是基礎設施垂直整合」「這是 SaaS 三支柱被鬆動」。這些是原作者或市場的解釋，不是事件本身。</p>
<p>第三、本文要教讀者帶走的框架，例如「三層擠壓」「資料平台前端化」「整併週期下的賽道判讀」。這些是本文推導。</p>
<p>Round 4 的 <code>#142</code> 已經處理「WRAP 內部分析喧賓奪主」；本卡補更上游的 source 問題：在開始寫正文前、先知道每句材料屬於哪一層。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步來源進稿前先做三欄標註">第一步：來源進稿前先做三欄標註</h3>
<p>把外部文章或 AI 轉寫草稿中的句子標成三欄：</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>
  </tbody>
</table>
<p>同一句若同時包含兩層、拆成兩句。例：「CoreWeave 收購 Bufstream，代表算力廠商開始垂直整合 data pipeline」應拆成：</p>
<ol>
<li>CoreWeave 收購 Bufstream。這是事實。</li>
<li>算力廠商往 data pipeline 延伸。這是本文判讀。</li>
</ol>
<h3 id="第二步事實進事件段判讀進-hypothesis推導進主體">第二步：事實進事件段、判讀進 hypothesis、推導進主體</h3>
<p>三層各有適合的位置：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>正文位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事實</td>
          <td>開頭與「事件本身」段</td>
      </tr>
      <tr>
          <td>原作者判讀</td>
          <td>Widen Options 的 prior 或背景敘事</td>
      </tr>
      <tr>
          <td>本文推導</td>
          <td>文章主體、長期影響、預警訊號、框架</td>
      </tr>
  </tbody>
</table>
<p>外部分析師的判讀可以幫助 widen hypothesis space，但它不該直接變成本 blog 的正文結論。正文結論要由本文的 evidence weight 與可遷移框架承擔。</p>
<h3 id="第三步合成-frame-要標成本文推導">第三步：合成 frame 要標成本文推導</h3>
<p>當文章把多個來源合成成一個框架時、要讓讀者知道這是本文的教學合成。寫法可以是：</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">本文把這個變化整理成三層：應用層 SaaS、新創供應商、知識工作者。</span></span></code></pre></div><p>這比「市場正在發生三層擠壓」精準，因為前者承認這是本文整理出的框架，後者容易讓讀者誤以為三層分類是外部事件本身。</p>
<h3 id="第四步引用來源時先寫概念再放-attribution">第四步：引用來源時先寫概念、再放 attribution</h3>
<p>來源 attribution 是可回溯支撐，不是段落主詞。先寫本文要教的概念，再補來源角色：</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">API 型產品若毛利薄、供應商會傾向用 enterprise contract 對沖收入波動。這個判讀可作為檢查供應商動機的 prior，不能直接當作本篇三層擠壓的主體。</span></span></code></pre></div><p>不要寫成「某某報告說 X，所以 X 是結論」。這會把來源權威放在概念之前，也讓讀者無法分辨 source claim 與本文推導。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="分析師-frame-被誤當事實">分析師 frame 被誤當事實</h3>
<p>外部文章的分類與比喻通常是作者的 frame。直接搬進正文，讀者會以為那是事件本身揭露的事實。後續若讀者回查來源找不到「三層擠壓」或「SaaS 三支柱鬆動」這些詞，會降低文章可信度。</p>
<h3 id="ai-改寫會放大-attribution-漂移">AI 改寫會放大 attribution 漂移</h3>
<p>AI 轉寫常把「某作者認為」壓縮成「這代表」。這個壓縮讓 claim 從觀點層滑到事實層。若沒有 source layering，改寫後的句子會更順、但 fidelity 更差。</p>
<h3 id="本文自己的貢獻變模糊">本文自己的貢獻變模糊</h3>
<p>教學型商業分析的價值是把事件整理成讀者可遷移的框架。若原作者判讀與本文推導混在一起，讀者看不出本文到底新增了什麼，只會覺得是另一篇摘要。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../fact-vs-derive-citation-layering/">#116 引用案例要分觀察層 / 判讀層</a></td>
          <td>#116 處理 case 引用，本卡把同一條分層原則套到外部分析文章 source</td>
      </tr>
      <tr>
          <td><a href="../cross-case-synthesized-frame-must-be-labeled/">#117 跨多個 case 合成的 frame 必須標為章節合成</a></td>
          <td>本卡是 #117 在 analyst source 的對應版本：跨來源合成 frame 要標成本文推導</td>
      </tr>
      <tr>
          <td><a href="../article-body-must-align-with-title-commitment/">#142 文章主體要對齊標題承諾</a></td>
          <td>Source layering 是 #142 的前置條件；先知道哪些是背景 prior，才知道哪些不該佔主體</td>
      </tr>
      <tr>
          <td><a href="../wrap-widen-options-strawman-risk/">#140 WRAP Widen Options 容易塌成稻草人 framing</a></td>
          <td>原作者判讀可作為 Widen Options prior，但不能被設成待打倒的稻草人或預設正解</td>
      </tr>
      <tr>
          <td><a href="../metadata-surface-in-writing-review/">#97 Metadata surface 要納入寫作 review 範圍</a></td>
          <td>Title / description 若把本文推導寫成事實，也會造成來源層漂移；metadata 也要跑 source layering</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>句子寫「這代表 X」、但 X 其實是分析師解釋</td>
          <td>改成「一種判讀是 X」或標成本文推導</td>
      </tr>
      <tr>
          <td>外部文章的比喻或分類被直接放進本篇標題</td>
          <td>確認這是原作者 frame 還是本文 frame</td>
      </tr>
      <tr>
          <td>AI 改寫後少了「某作者認為」「市場敘事」等 attribution</td>
          <td>回原文補回 source layer</td>
      </tr>
      <tr>
          <td>引用來源只寫機構名、沒有具體文章或可驗證出處</td>
          <td>刪掉具名 source，改成一般 prior 或重新查證</td>
      </tr>
      <tr>
          <td>讀者問「這是事件事實、原文觀點、還是你自己的推導」</td>
          <td>Source layering 失敗，重寫段落主詞與 attribution</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：外部分析文章進入教學寫作前、先拆成事實、作者判讀、本文推導。三層分清楚，文章才有可回溯性，也才看得出本文真正教了什麼。</p>
]]></content:encoded></item><item><title>跨領域分析要先定位讀者層級、再決定術語密度</title><link>https://tarrragon.github.io/blog/report/cross-domain-reader-level-alignment/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cross-domain-reader-level-alignment/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>跨領域分析的讀者層級要先定位、再決定術語密度。把商業分析寫給工程背景讀者時、不能繼承原分析文章的 VC / founder / industry insider 讀者假設；要把原文的高密度術語與壓縮因果鏈降到目標讀者可複製判讀的層級。&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>可連續使用 CAC / LTV / ARR / gross margin&lt;/td>
 &lt;td>首次出現要就地解釋或連到卡片&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>因果鏈&lt;/td>
 &lt;td>可跳過中間步驟&lt;/td>
 &lt;td>每段只推一到兩個因果步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>數字&lt;/td>
 &lt;td>可用百分比與估值倍數暗示&lt;/td>
 &lt;td>要補公式、分母、比較基準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文章目標&lt;/td>
 &lt;td>展示判斷深度&lt;/td>
 &lt;td>教讀者如何重做判斷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「讀者若沒有商業分析背景，能不能用這段推導去分析下一個事件？」不能，就要降一級。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>&lt;code>content/business/reading-frameworks/writing-down-a-level.md&lt;/code> 是從 business case-analyses 的 Round 5 修文抽出來的。當時讀者回饋指出，文章雖然比第一版更像教學，但仍太像寫給 VC / founder 的 deep blog：三句內連續塞進多個商業術語，且因果鏈跨太多步。&lt;/p>
&lt;p>例如 Claude for Legal 的第一層擠壓原本把 enterprise license、API margin、長約、垂直流程、法律風險等概念壓在少數句子裡。工程背景讀者可能知道 API、SaaS、workflow，但未必直覺知道「為什麼毛利結構會推動供應商賣 enterprise contract」。若不拆開，讀者只能記住結論，無法複製推導。&lt;/p>
&lt;p>後續 &lt;code>b9c5f06&lt;/code> 新增 reading framework，&lt;code>8c67ab6&lt;/code> 再把 FDE 與 Bufstream 文章的 5 段高密度內容降一級，形成穩定規則：跨領域文章要先判斷原文寫給誰，再決定本站要降到哪裡。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步先辨識原文的-reader-contract">第一步：先辨識原文的 reader contract&lt;/h3>
&lt;p>外部商業文章常見 reader contract：&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>VC / founder 文&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>MBA 教材&lt;/td>
 &lt;td>有基礎商業概念但不熟特定產業者&lt;/td>
 &lt;td>概念明確、例子完整、因果鏈可追&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>本站 business case-analyses 的目標更接近「工程背景讀者讀 MBA 教材」：保留商業分析深度，但不預設讀者熟悉投資圈 shorthand。&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">API margin → enterprise ARR → vertical workflow lock-in&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這串對 VC 讀者可能順，但對工程背景讀者需要拆成：&lt;/p>
&lt;ol>
&lt;li>API 型產品的收入跟成本如何變動。&lt;/li>
&lt;li>為什麼長約能降低收入波動。&lt;/li>
&lt;li>為什麼垂直流程能讓客戶更難替換。&lt;/li>
&lt;/ol>
&lt;h3 id="第三步用因果鏈步長決定段落切分">第三步：用因果鏈步長決定段落切分&lt;/h3>
&lt;p>每段只推一到兩步。若一句話需要讀者同時理解「成本結構、銷售模式、採購風險、workflow switching cost」，拆成多段。&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>A 導致 B、進而 C&lt;/td>
 &lt;td>第一段 A→B，第二段 B→C&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用術語承接術語&lt;/td>
 &lt;td>每個術語首次出現補一行功能描述&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用比喻代替機制&lt;/td>
 &lt;td>先寫機制，再用比喻輔助&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="第四步用卡片承擔術語不讓正文變字典">第四步：用卡片承擔術語，不讓正文變字典&lt;/h3>
&lt;p>降一級不等於每次都在正文長篇定義名詞。若術語會反覆影響判斷成本，就建卡或連既有卡；正文只保留當下推導需要的最小解釋。&lt;/p>
&lt;p>例：&lt;code>unit economics&lt;/code>、&lt;code>switching cost&lt;/code>、&lt;code>vertical SaaS&lt;/code> 這類詞，若每篇都重新解釋，正文會膨脹；卡片負責概念，文章負責事件推導。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="讀者只能記名詞不能複製判讀">讀者只能記名詞，不能複製判讀&lt;/h3>
&lt;p>高密度商業語言會讓文章看起來專業，但讀者若無法把術語放回因果鏈，只會記住「FDE 代表 SaaS 三支柱鬆動」這種結論句。下一次遇到不同市場事件時，判斷工具無法遷移。&lt;/p>
&lt;h3 id="ai-改寫會保留原文讀者假設">AI 改寫會保留原文讀者假設&lt;/h3>
&lt;p>AI 常把原文風格重寫得更流暢，但不會自動調整 reader contract。原文若寫給 VC / founder，改寫後仍可能保留高密度 shorthand，只是中文更順。&lt;/p>
&lt;h3 id="文章篇幅變短但理解成本變高">文章篇幅變短，但理解成本變高&lt;/h3>
&lt;p>跨領域文章最常見的錯覺是「短 = 清楚」。實際上，短句若省略中間因果，讀者需要自行補推導。對目標讀者而言，補推導的成本比多讀兩段更高。&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>&lt;a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證&lt;/a>&lt;/td>
 &lt;td>本卡把讀者旅程落到「跨領域讀者能不能重做判斷」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../prose-self-contained-without-code-reference/">#113 商業邏輯論述要 self-contained&lt;/a>&lt;/td>
 &lt;td>#113 處理不依賴 code，本卡處理不依賴原文讀者的商業背景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點&lt;/a>&lt;/td>
 &lt;td>降一級時仍要保留原文錨點，避免中文術語變成不可回溯的自由翻譯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性&lt;/a>&lt;/td>
 &lt;td>本卡補 reader-level 軸；多輪 review 不能只看結構，還要看目標讀者是否能吸收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../external-analysis-source-layering/">#143 外部分析文章要先拆成事實、作者判讀、本文推導&lt;/a>&lt;/td>
 &lt;td>Source 分層回答材料可信度，本卡回答材料要降到哪個讀者層級&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>三句內連續出現三個以上商業術語&lt;/td>
 &lt;td>拆段、補首次解釋、或連到知識卡&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一句話跨過三個以上因果步&lt;/td>
 &lt;td>改成第一步、第二步、第三步的段落推進&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文章讀起來像 VC memo 或 founder newsletter&lt;/td>
 &lt;td>降到工程背景讀者可讀的 MBA 教材層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者能複述結論、但說不出判斷怎麼來&lt;/td>
 &lt;td>補中間機制、公式、數字或比較基準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AI 改寫後文字變順、但術語密度沒下降&lt;/td>
 &lt;td>重新跑 reader-level pass，不把流暢度當可讀性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：跨領域分析的寫作目標是讓讀者複製判讀，不是讓原文變順。先定位原文與目標讀者的層級差，再調整術語密度與因果鏈步長。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>跨領域分析的讀者層級要先定位、再決定術語密度。把商業分析寫給工程背景讀者時、不能繼承原分析文章的 VC / founder / industry insider 讀者假設；要把原文的高密度術語與壓縮因果鏈降到目標讀者可複製判讀的層級。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>高階同領域讀者</th>
          <th>跨領域教學讀者</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>術語</td>
          <td>可連續使用 CAC / LTV / ARR / gross margin</td>
          <td>首次出現要就地解釋或連到卡片</td>
      </tr>
      <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>判別問題：「讀者若沒有商業分析背景，能不能用這段推導去分析下一個事件？」不能，就要降一級。</p>
<hr>
<h2 id="情境">情境</h2>
<p><code>content/business/reading-frameworks/writing-down-a-level.md</code> 是從 business case-analyses 的 Round 5 修文抽出來的。當時讀者回饋指出，文章雖然比第一版更像教學，但仍太像寫給 VC / founder 的 deep blog：三句內連續塞進多個商業術語，且因果鏈跨太多步。</p>
<p>例如 Claude for Legal 的第一層擠壓原本把 enterprise license、API margin、長約、垂直流程、法律風險等概念壓在少數句子裡。工程背景讀者可能知道 API、SaaS、workflow，但未必直覺知道「為什麼毛利結構會推動供應商賣 enterprise contract」。若不拆開，讀者只能記住結論，無法複製推導。</p>
<p>後續 <code>b9c5f06</code> 新增 reading framework，<code>8c67ab6</code> 再把 FDE 與 Bufstream 文章的 5 段高密度內容降一級，形成穩定規則：跨領域文章要先判斷原文寫給誰，再決定本站要降到哪裡。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步先辨識原文的-reader-contract">第一步：先辨識原文的 reader contract</h3>
<p>外部商業文章常見 reader contract：</p>
<table>
  <thead>
      <tr>
          <th>原文類型</th>
          <th>預設讀者</th>
          <th>寫作特徵</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>大眾財經新聞</td>
          <td>一般投資讀者</td>
          <td>事件摘要、股價、短期影響</td>
      </tr>
      <tr>
          <td>VC / founder 文</td>
          <td>創辦人、投資人、策略工作者</td>
          <td>術語密度高、暗示多、判斷快</td>
      </tr>
      <tr>
          <td>產業內部文</td>
          <td>已在該市場工作的人</td>
          <td>預設供應鏈、客戶型態、競爭格局</td>
      </tr>
      <tr>
          <td>MBA 教材</td>
          <td>有基礎商業概念但不熟特定產業者</td>
          <td>概念明確、例子完整、因果鏈可追</td>
      </tr>
  </tbody>
</table>
<p>本站 business case-analyses 的目標更接近「工程背景讀者讀 MBA 教材」：保留商業分析深度，但不預設讀者熟悉投資圈 shorthand。</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">API margin → enterprise ARR → vertical workflow lock-in</span></span></code></pre></div><p>這串對 VC 讀者可能順，但對工程背景讀者需要拆成：</p>
<ol>
<li>API 型產品的收入跟成本如何變動。</li>
<li>為什麼長約能降低收入波動。</li>
<li>為什麼垂直流程能讓客戶更難替換。</li>
</ol>
<h3 id="第三步用因果鏈步長決定段落切分">第三步：用因果鏈步長決定段落切分</h3>
<p>每段只推一到兩步。若一句話需要讀者同時理解「成本結構、銷售模式、採購風險、workflow switching cost」，拆成多段。</p>
<table>
  <thead>
      <tr>
          <th>原句壓縮方式</th>
          <th>降級後寫法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>A 導致 B、進而 C</td>
          <td>第一段 A→B，第二段 B→C</td>
      </tr>
      <tr>
          <td>用術語承接術語</td>
          <td>每個術語首次出現補一行功能描述</td>
      </tr>
      <tr>
          <td>用比喻代替機制</td>
          <td>先寫機制，再用比喻輔助</td>
      </tr>
  </tbody>
</table>
<h3 id="第四步用卡片承擔術語不讓正文變字典">第四步：用卡片承擔術語，不讓正文變字典</h3>
<p>降一級不等於每次都在正文長篇定義名詞。若術語會反覆影響判斷成本，就建卡或連既有卡；正文只保留當下推導需要的最小解釋。</p>
<p>例：<code>unit economics</code>、<code>switching cost</code>、<code>vertical SaaS</code> 這類詞，若每篇都重新解釋，正文會膨脹；卡片負責概念，文章負責事件推導。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="讀者只能記名詞不能複製判讀">讀者只能記名詞，不能複製判讀</h3>
<p>高密度商業語言會讓文章看起來專業，但讀者若無法把術語放回因果鏈，只會記住「FDE 代表 SaaS 三支柱鬆動」這種結論句。下一次遇到不同市場事件時，判斷工具無法遷移。</p>
<h3 id="ai-改寫會保留原文讀者假設">AI 改寫會保留原文讀者假設</h3>
<p>AI 常把原文風格重寫得更流暢，但不會自動調整 reader contract。原文若寫給 VC / founder，改寫後仍可能保留高密度 shorthand，只是中文更順。</p>
<h3 id="文章篇幅變短但理解成本變高">文章篇幅變短，但理解成本變高</h3>
<p>跨領域文章最常見的錯覺是「短 = 清楚」。實際上，短句若省略中間因果，讀者需要自行補推導。對目標讀者而言，補推導的成本比多讀兩段更高。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證</a></td>
          <td>本卡把讀者旅程落到「跨領域讀者能不能重做判斷」</td>
      </tr>
      <tr>
          <td><a href="../prose-self-contained-without-code-reference/">#113 商業邏輯論述要 self-contained</a></td>
          <td>#113 處理不依賴 code，本卡處理不依賴原文讀者的商業背景</td>
      </tr>
      <tr>
          <td><a href="../terminology-keeps-original-anchor/">#107 術語翻譯要保留原文錨點</a></td>
          <td>降一級時仍要保留原文錨點，避免中文術語變成不可回溯的自由翻譯</td>
      </tr>
      <tr>
          <td><a href="../writing-review-multi-axis-completeness/">#126 寫作 review 是多軸完整性</a></td>
          <td>本卡補 reader-level 軸；多輪 review 不能只看結構，還要看目標讀者是否能吸收</td>
      </tr>
      <tr>
          <td><a href="../external-analysis-source-layering/">#143 外部分析文章要先拆成事實、作者判讀、本文推導</a></td>
          <td>Source 分層回答材料可信度，本卡回答材料要降到哪個讀者層級</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>一句話跨過三個以上因果步</td>
          <td>改成第一步、第二步、第三步的段落推進</td>
      </tr>
      <tr>
          <td>文章讀起來像 VC memo 或 founder newsletter</td>
          <td>降到工程背景讀者可讀的 MBA 教材層</td>
      </tr>
      <tr>
          <td>讀者能複述結論、但說不出判斷怎麼來</td>
          <td>補中間機制、公式、數字或比較基準</td>
      </tr>
      <tr>
          <td>AI 改寫後文字變順、但術語密度沒下降</td>
          <td>重新跑 reader-level pass，不把流暢度當可讀性</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：跨領域分析的寫作目標是讓讀者複製判讀，不是讓原文變順。先定位原文與目標讀者的層級差，再調整術語密度與因果鏈步長。</p>
]]></content:encoded></item><item><title>外部分析改寫的交付物是可遷移框架、不是風格轉換</title><link>https://tarrragon.github.io/blog/report/analysis-rewrite-must-deliver-transferable-framework/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/analysis-rewrite-must-deliver-transferable-framework/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>外部分析改寫的交付物是可遷移框架、不是風格轉換。把其他分析師文章交給 AI 改寫時，任務目標不能停在「改成本站語氣」「更正向」「更像教學」；真正要交付的是讀者能帶到下一個市場事件使用的判讀工具。&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>把 WRAP process 改成教學章節&lt;/td>
 &lt;td>可能仍偏離標題承諾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>框架轉換&lt;/td>
 &lt;td>事件訊號、機制、風險、預警、遷移用法&lt;/td>
 &lt;td>讀者能用來分析下一個事件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別問題：「讀者讀完後，是否多了一個可重用的判斷問題或檢查表？」沒有，就還只是改寫。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>&lt;code>content/business/&lt;/code> 的建立過程是一場從「外部分析文章」到「本站教學型商業知識」的轉換實驗。commit 演變顯示，早期版本雖然有 WRAP、知識卡與 case analyses，但仍多次被讀者 feedback 拉回核心問題：&lt;/p>
&lt;ul>
&lt;li>&lt;code>#140&lt;/code>：Widen Options 不能用稻草人修辭展示作者結論。&lt;/li>
&lt;li>&lt;code>#141&lt;/code>：WRAP 是內部工具，不能直接當文章章節。&lt;/li>
&lt;li>&lt;code>#142&lt;/code>：即使章節標題改了，正文仍要對齊標題承諾。&lt;/li>
&lt;li>&lt;code>writing-down-a-level&lt;/code>：目標讀者是工程背景讀者，不是原文的投資人或創辦人。&lt;/li>
&lt;/ul>
&lt;p>這些問題共同指向一件事：AI 轉換文章風格不夠。文章要從「原作者如何看這件事」轉成「本站讀者以後如何判讀同類事件」。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="第一步把任務定義成-frame-extraction">第一步：把任務定義成 frame extraction&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>這篇原文用什麼 frame 看事件？&lt;/td>
 &lt;td>辨識原作者判讀，不直接繼承&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>這個 frame 對本站讀者有沒有可遷移價值？&lt;/td>
 &lt;td>決定是否值得寫成文章&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>若要遷移，讀者下次要看哪些訊號？&lt;/td>
 &lt;td>把評論轉成判讀工具&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>哪些術語需要卡片支撐？&lt;/td>
 &lt;td>避免正文變成術語堆疊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>若事件只能產出「某公司發生某事」的摘要、沒有可遷移框架，就放在筆記，不硬寫成 case-analysis。&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;/code>&lt;/pre>&lt;/div>&lt;p>這是責任順序，可依標題承諾調整。事件段讓讀者進場；機制段教為什麼；長期影響處理時間軸；預警訊號讓框架可被反證；可遷移框架讓讀者能帶走。&lt;/p>
&lt;p>若某篇文章的標題承諾需要不同順序，可以調整；但最後仍要回答「這個事件教讀者下次看什麼」。&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>AI legal tools 會擠壓知識工作者&lt;/td>
 &lt;td>這個工具在擠壓應用層、新創、工作者哪一層？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>FDE 是資料平台的新戰場&lt;/td>
 &lt;td>它是否鬆動 SaaS 的 distribution、workflow、data 三支柱？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>收購代表基礎設施整併&lt;/td>
 &lt;td>這是純收購、賽道整併，還是算力廠商垂直整合？&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判斷問題比結論更有價值，因為問題可以帶到下一個 case。&lt;/p>
&lt;h3 id="第四步用預警訊號保護框架">第四步：用預警訊號保護框架&lt;/h3>
&lt;p>可遷移框架需要知道何時失效。每篇 case-analysis 至少要有一段預警訊號，列出哪些觀察會推翻或削弱本文判讀。&lt;/p>
&lt;p>沒有預警訊號的框架只是一組漂亮分類。讀者需要知道「何時重新評估」，才會把框架當工具，而不是把它當口號。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="文章會變成原文摘要">文章會變成原文摘要&lt;/h3>
&lt;p>AI 很擅長把原文改成不同語氣，但摘要仍然沿著原文的 frame 走。讀者得到的是「另一個版本的原文」，不是本站累積出的知識單元。&lt;/p>
&lt;h3 id="事件評論不可重用">事件評論不可重用&lt;/h3>
&lt;p>若文章只回答「這件事代表什麼」，下次遇到不同公司、不同市場、不同產品時，讀者還是要從頭判斷。可遷移框架要回答「下次遇到相似事件，我要看哪些訊號」。&lt;/p>
&lt;h3 id="寫作規範會被誤解成表面風格">寫作規範會被誤解成表面風格&lt;/h3>
&lt;p>正向陳述、核心原則先行、商業邏輯先於 CASE 不是語氣規則；它們是把思考過程變成可重用知識的結構規則。若只做風格轉換，會通過部分表面檢查，但仍不符合知識庫目標。&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>&lt;a href="../wrap-as-internal-tool-not-section-structure/">#141 WRAP 是寫作者的內部工具&lt;/a>&lt;/td>
 &lt;td>#141 處理 process 不能外露，本卡處理 process 之後要交付什麼&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../article-body-must-align-with-title-commitment/">#142 文章主體要對齊標題承諾&lt;/a>&lt;/td>
 &lt;td>標題承諾應該指向可遷移框架；若標題只承諾事件評論，文章容易停在摘要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../external-analysis-source-layering/">#143 外部分析文章要先拆成事實、作者判讀、本文推導&lt;/a>&lt;/td>
 &lt;td>Source 分層是框架轉換的前置步驟；先知道哪些是原文觀點，才能抽出本站框架&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../cross-domain-reader-level-alignment/">#144 跨領域分析要先定位讀者層級&lt;/a>&lt;/td>
 &lt;td>可遷移框架要用目標讀者能操作的語言表達，不能保留原文的高密度 shorthand&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../cards-as-living-system-iteration/">#81 卡片系統的迭代浮現&lt;/a>&lt;/td>
 &lt;td>一篇 case-analysis 產生的框架若反覆出現，後續可升級成知識卡或 reading framework&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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>Prompt 只說「改成我們的風格」&lt;/td>
 &lt;td>改成「抽出可遷移判讀框架」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文章結尾只有事件結論&lt;/td>
 &lt;td>補「下次遇到同類事件要看什麼」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者讀完只能說出某公司發生什麼&lt;/td>
 &lt;td>補訊號、機制、風險、預警與路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文章沒有預警訊號或 Tripwire&lt;/td>
 &lt;td>補失效條件，讓框架可被反證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文章大量沿用原文 frame&lt;/td>
 &lt;td>回到 source layering，區分原文判讀與本文推導&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：外部分析改寫的交付物是讀者可遷移的判讀框架，把原文變順只是表層。沒有框架、預警與下一步路由，文章仍停在摘要層。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>外部分析改寫的交付物是可遷移框架、不是風格轉換。把其他分析師文章交給 AI 改寫時，任務目標不能停在「改成本站語氣」「更正向」「更像教學」；真正要交付的是讀者能帶到下一個市場事件使用的判讀工具。</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>把 WRAP process 改成教學章節</td>
          <td>可能仍偏離標題承諾</td>
      </tr>
      <tr>
          <td>框架轉換</td>
          <td>事件訊號、機制、風險、預警、遷移用法</td>
          <td>讀者能用來分析下一個事件</td>
      </tr>
  </tbody>
</table>
<p>判別問題：「讀者讀完後，是否多了一個可重用的判斷問題或檢查表？」沒有，就還只是改寫。</p>
<hr>
<h2 id="情境">情境</h2>
<p><code>content/business/</code> 的建立過程是一場從「外部分析文章」到「本站教學型商業知識」的轉換實驗。commit 演變顯示，早期版本雖然有 WRAP、知識卡與 case analyses，但仍多次被讀者 feedback 拉回核心問題：</p>
<ul>
<li><code>#140</code>：Widen Options 不能用稻草人修辭展示作者結論。</li>
<li><code>#141</code>：WRAP 是內部工具，不能直接當文章章節。</li>
<li><code>#142</code>：即使章節標題改了，正文仍要對齊標題承諾。</li>
<li><code>writing-down-a-level</code>：目標讀者是工程背景讀者，不是原文的投資人或創辦人。</li>
</ul>
<p>這些問題共同指向一件事：AI 轉換文章風格不夠。文章要從「原作者如何看這件事」轉成「本站讀者以後如何判讀同類事件」。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="第一步把任務定義成-frame-extraction">第一步：把任務定義成 frame extraction</h3>
<p>輸入外部文章時，先問：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>作用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這篇原文用什麼 frame 看事件？</td>
          <td>辨識原作者判讀，不直接繼承</td>
      </tr>
      <tr>
          <td>這個 frame 對本站讀者有沒有可遷移價值？</td>
          <td>決定是否值得寫成文章</td>
      </tr>
      <tr>
          <td>若要遷移，讀者下次要看哪些訊號？</td>
          <td>把評論轉成判讀工具</td>
      </tr>
      <tr>
          <td>哪些術語需要卡片支撐？</td>
          <td>避免正文變成術語堆疊</td>
      </tr>
  </tbody>
</table>
<p>若事件只能產出「某公司發生某事」的摘要、沒有可遷移框架，就放在筆記，不硬寫成 case-analysis。</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></code></pre></div><p>這是責任順序，可依標題承諾調整。事件段讓讀者進場；機制段教為什麼；長期影響處理時間軸；預警訊號讓框架可被反證；可遷移框架讓讀者能帶走。</p>
<p>若某篇文章的標題承諾需要不同順序，可以調整；但最後仍要回答「這個事件教讀者下次看什麼」。</p>
<h3 id="第三步把原文觀點轉成判斷問題">第三步：把原文觀點轉成判斷問題</h3>
<p>風格轉換會把原文句子改得更順；框架轉換會把原文觀點改成讀者可問的問題。</p>
<table>
  <thead>
      <tr>
          <th>原文觀點型句子</th>
          <th>框架型問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AI legal tools 會擠壓知識工作者</td>
          <td>這個工具在擠壓應用層、新創、工作者哪一層？</td>
      </tr>
      <tr>
          <td>FDE 是資料平台的新戰場</td>
          <td>它是否鬆動 SaaS 的 distribution、workflow、data 三支柱？</td>
      </tr>
      <tr>
          <td>收購代表基礎設施整併</td>
          <td>這是純收購、賽道整併，還是算力廠商垂直整合？</td>
      </tr>
  </tbody>
</table>
<p>判斷問題比結論更有價值，因為問題可以帶到下一個 case。</p>
<h3 id="第四步用預警訊號保護框架">第四步：用預警訊號保護框架</h3>
<p>可遷移框架需要知道何時失效。每篇 case-analysis 至少要有一段預警訊號，列出哪些觀察會推翻或削弱本文判讀。</p>
<p>沒有預警訊號的框架只是一組漂亮分類。讀者需要知道「何時重新評估」，才會把框架當工具，而不是把它當口號。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="文章會變成原文摘要">文章會變成原文摘要</h3>
<p>AI 很擅長把原文改成不同語氣，但摘要仍然沿著原文的 frame 走。讀者得到的是「另一個版本的原文」，不是本站累積出的知識單元。</p>
<h3 id="事件評論不可重用">事件評論不可重用</h3>
<p>若文章只回答「這件事代表什麼」，下次遇到不同公司、不同市場、不同產品時，讀者還是要從頭判斷。可遷移框架要回答「下次遇到相似事件，我要看哪些訊號」。</p>
<h3 id="寫作規範會被誤解成表面風格">寫作規範會被誤解成表面風格</h3>
<p>正向陳述、核心原則先行、商業邏輯先於 CASE 不是語氣規則；它們是把思考過程變成可重用知識的結構規則。若只做風格轉換，會通過部分表面檢查，但仍不符合知識庫目標。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../wrap-as-internal-tool-not-section-structure/">#141 WRAP 是寫作者的內部工具</a></td>
          <td>#141 處理 process 不能外露，本卡處理 process 之後要交付什麼</td>
      </tr>
      <tr>
          <td><a href="../article-body-must-align-with-title-commitment/">#142 文章主體要對齊標題承諾</a></td>
          <td>標題承諾應該指向可遷移框架；若標題只承諾事件評論，文章容易停在摘要</td>
      </tr>
      <tr>
          <td><a href="../external-analysis-source-layering/">#143 外部分析文章要先拆成事實、作者判讀、本文推導</a></td>
          <td>Source 分層是框架轉換的前置步驟；先知道哪些是原文觀點，才能抽出本站框架</td>
      </tr>
      <tr>
          <td><a href="../cross-domain-reader-level-alignment/">#144 跨領域分析要先定位讀者層級</a></td>
          <td>可遷移框架要用目標讀者能操作的語言表達，不能保留原文的高密度 shorthand</td>
      </tr>
      <tr>
          <td><a href="../cards-as-living-system-iteration/">#81 卡片系統的迭代浮現</a></td>
          <td>一篇 case-analysis 產生的框架若反覆出現，後續可升級成知識卡或 reading framework</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prompt 只說「改成我們的風格」</td>
          <td>改成「抽出可遷移判讀框架」</td>
      </tr>
      <tr>
          <td>文章結尾只有事件結論</td>
          <td>補「下次遇到同類事件要看什麼」</td>
      </tr>
      <tr>
          <td>讀者讀完只能說出某公司發生什麼</td>
          <td>補訊號、機制、風險、預警與路由</td>
      </tr>
      <tr>
          <td>文章沒有預警訊號或 Tripwire</td>
          <td>補失效條件，讓框架可被反證</td>
      </tr>
      <tr>
          <td>文章大量沿用原文 frame</td>
          <td>回到 source layering，區分原文判讀與本文推導</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：外部分析改寫的交付物是讀者可遷移的判讀框架，把原文變順只是表層。沒有框架、預警與下一步路由，文章仍停在摘要層。</p>
]]></content:encoded></item><item><title>案例庫不對齊章節主題時用反向追問取代強掛</title><link>https://tarrragon.github.io/blog/report/case-misalignment-reverse-inquiry/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/case-misalignment-reverse-inquiry/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>案例庫主軸跟章節主題不在同一維度時、引用框架要從「正向掛入」切換到「反向追問」。正向掛入適用於「案例直接示範章節主題」、反向追問適用於「案例庫主軸是 A、章節主題是 B、A 與 B 雖正交但 A 可作為 B 重要性的反證」。&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>「[case]：看 X 如何展示 Y、對照本章 Z 段」&lt;/td>
 &lt;td>對齊時無風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反向追問&lt;/td>
 &lt;td>案例主軸不對應、有反向對照價值&lt;/td>
 &lt;td>「[case] 主軸是 A、不直接示範 B；反問『這條撞牆是否被 B 放大』」&lt;/td>
 &lt;td>仍可能像強掛、要明示分層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>強掛&lt;/td>
 &lt;td>不對齊時硬用正向句型（誤用）&lt;/td>
 &lt;td>「[case]：看 X 如何決定 Y」、X、Y 在 case 中無具體段落支撐&lt;/td>
 &lt;td>reviewer fact-check 一查就抓出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不對齊情境下若硬用正向句型、就會落到強掛。Reviewer 第一輪 audit 抓出後、修正成本是「全段重寫 + 重做案例對照」。先判讀對不對齊、再決定框架、比事後重寫便宜。&lt;/p>
&lt;h2 id="為什麼強掛會發生">為什麼強掛會發生&lt;/h2>
&lt;p>寫作者面對「案例回寫」段時、預設「每章都該有 3-5 個 case 引用」、案例庫實際只有 1-2 個直接相關時、剩下會用 stretch 句型硬掛。stretch 的徵兆通常有三個：&lt;/p>
&lt;ul>
&lt;li>用案例提到的 vendor / 服務名稱掛、不用案例揭露的機制掛&lt;/li>
&lt;li>描述句型抽象、避免具體斷言：「看 X 如何決定 Y」、回查 case 找不到「怎麼決定」&lt;/li>
&lt;li>把案例次要訊息當主軸：case 主軸是 A、引用句只提 B、B 在 case 是一筆帶過&lt;/li>
&lt;/ul>
&lt;p>背後動機是「想讓段落看起來完整」、而非「想讓讀者看到證據」。3-5 個 bullet 變成內在配額、引用變成填空、不是工具。這條動機跟 &lt;a href="https://tarrragon.github.io/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度&lt;/a> 的成因同源 — 模板從「輔助結構」滑落為「強制配額」。&lt;/p>
&lt;h2 id="反向追問步驟">反向追問步驟&lt;/h2>
&lt;p>不對齊時、反向追問的標準操作分三步：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>誠實標示案例庫主軸差異&lt;/strong>：開段直接寫明「本案例庫主軸是 A、直接以 B 為主題的案例較少」、把案例庫的限制當 first-class 訊息傳給讀者。讀者知道後續引用會用反向讀法、而非把它當成直接示範。&lt;/li>
&lt;li>&lt;strong>把案例當「沒做 B 的後果」&lt;/strong>：每個 case 改寫成「在沒有先用 B 收回壓力的前提下、團隊走了哪條路（遷移 / scale-out / vendor 升級）」、case 因此變成 B 重要性的反證。寫作意圖從「示範 B」轉成「示範沒做 B 的代價」。&lt;/li>
&lt;li>&lt;strong>明示分層追問&lt;/strong>：在引用描述句裡寫明追問 — 讀者讀完 case 應主動問「這條撞牆是否被 B 放大」。把追問句寫進引用、讓讀者知道這是反向讀法、而非把 case 當對齊。&lt;/li>
&lt;/ol>
&lt;p>三步驟做完、案例段仍保留同樣多的引用、但語意誠實、reviewer fact-check 不會抓出不符。&lt;/p>
&lt;h2 id="case">Case&lt;/h2>
&lt;p>backend/01.13 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">查詢反模式章節&lt;/a> 在 reviewer audit 階段的具體經驗：&lt;/p>
&lt;p>原寫法：3 個 09 模組 case（DoorDash / Zomato / Standard Chartered）被強掛在「Long-Running Transaction」「Query 預算」這類 application-layer query 反模式主題上。&lt;/p>
&lt;p>Reviewer fact-check 結果：&lt;/p>
&lt;ul>
&lt;li>DoorDash case 主軸是 single-primary 寫入吞吐瓶頸、跟 long transaction 無關&lt;/li>
&lt;li>Zomato case 主軸是 TiDB → DynamoDB 遷移、case 完全沒有 query budget 討論&lt;/li>
&lt;li>Standard Chartered case 主軸是合規驅動容量規劃、跟 N+1 / query 預算 stretch&lt;/li>
&lt;/ul>
&lt;p>2.5 / 3 case 的引用描述跟 case 原文不符。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>案例庫主軸跟章節主題不在同一維度時、引用框架要從「正向掛入」切換到「反向追問」。正向掛入適用於「案例直接示範章節主題」、反向追問適用於「案例庫主軸是 A、章節主題是 B、A 與 B 雖正交但 A 可作為 B 重要性的反證」。</p>
<table>
  <thead>
      <tr>
          <th>引用框架</th>
          <th>適用情境</th>
          <th>句型骨架</th>
          <th>典型風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>正向掛入</td>
          <td>案例直接示範章節主題</td>
          <td>「[case]：看 X 如何展示 Y、對照本章 Z 段」</td>
          <td>對齊時無風險</td>
      </tr>
      <tr>
          <td>反向追問</td>
          <td>案例主軸不對應、有反向對照價值</td>
          <td>「[case] 主軸是 A、不直接示範 B；反問『這條撞牆是否被 B 放大』」</td>
          <td>仍可能像強掛、要明示分層</td>
      </tr>
      <tr>
          <td>強掛</td>
          <td>不對齊時硬用正向句型（誤用）</td>
          <td>「[case]：看 X 如何決定 Y」、X、Y 在 case 中無具體段落支撐</td>
          <td>reviewer fact-check 一查就抓出</td>
      </tr>
  </tbody>
</table>
<p>不對齊情境下若硬用正向句型、就會落到強掛。Reviewer 第一輪 audit 抓出後、修正成本是「全段重寫 + 重做案例對照」。先判讀對不對齊、再決定框架、比事後重寫便宜。</p>
<h2 id="為什麼強掛會發生">為什麼強掛會發生</h2>
<p>寫作者面對「案例回寫」段時、預設「每章都該有 3-5 個 case 引用」、案例庫實際只有 1-2 個直接相關時、剩下會用 stretch 句型硬掛。stretch 的徵兆通常有三個：</p>
<ul>
<li>用案例提到的 vendor / 服務名稱掛、不用案例揭露的機制掛</li>
<li>描述句型抽象、避免具體斷言：「看 X 如何決定 Y」、回查 case 找不到「怎麼決定」</li>
<li>把案例次要訊息當主軸：case 主軸是 A、引用句只提 B、B 在 case 是一筆帶過</li>
</ul>
<p>背後動機是「想讓段落看起來完整」、而非「想讓讀者看到證據」。3-5 個 bullet 變成內在配額、引用變成填空、不是工具。這條動機跟 <a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度</a> 的成因同源 — 模板從「輔助結構」滑落為「強制配額」。</p>
<h2 id="反向追問步驟">反向追問步驟</h2>
<p>不對齊時、反向追問的標準操作分三步：</p>
<ol>
<li><strong>誠實標示案例庫主軸差異</strong>：開段直接寫明「本案例庫主軸是 A、直接以 B 為主題的案例較少」、把案例庫的限制當 first-class 訊息傳給讀者。讀者知道後續引用會用反向讀法、而非把它當成直接示範。</li>
<li><strong>把案例當「沒做 B 的後果」</strong>：每個 case 改寫成「在沒有先用 B 收回壓力的前提下、團隊走了哪條路（遷移 / scale-out / vendor 升級）」、case 因此變成 B 重要性的反證。寫作意圖從「示範 B」轉成「示範沒做 B 的代價」。</li>
<li><strong>明示分層追問</strong>：在引用描述句裡寫明追問 — 讀者讀完 case 應主動問「這條撞牆是否被 B 放大」。把追問句寫進引用、讓讀者知道這是反向讀法、而非把 case 當對齊。</li>
</ol>
<p>三步驟做完、案例段仍保留同樣多的引用、但語意誠實、reviewer fact-check 不會抓出不符。</p>
<h2 id="case">Case</h2>
<p>backend/01.13 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">查詢反模式章節</a> 在 reviewer audit 階段的具體經驗：</p>
<p>原寫法：3 個 09 模組 case（DoorDash / Zomato / Standard Chartered）被強掛在「Long-Running Transaction」「Query 預算」這類 application-layer query 反模式主題上。</p>
<p>Reviewer fact-check 結果：</p>
<ul>
<li>DoorDash case 主軸是 single-primary 寫入吞吐瓶頸、跟 long transaction 無關</li>
<li>Zomato case 主軸是 TiDB → DynamoDB 遷移、case 完全沒有 query budget 討論</li>
<li>Standard Chartered case 主軸是合規驅動容量規劃、跟 N+1 / query 預算 stretch</li>
</ul>
<p>2.5 / 3 case 的引用描述跟 case 原文不符。</p>
<p>修正：改用反向追問框架。開段標示「09 案例庫主軸是規模、vendor 與容量壓力、直接以 query 反模式為主題的案例較少」、三個 case 重寫成「遷移 / scale-out / 合規容量規劃前、是否該先用 query 反模式收回單機容量」的反向追問。Reviewer 二輪通過、3 個 case 全保留、語意誠實。</p>
<p>這個 case 揭露的核心：reviewer 抓到的不是「引用太多」、是「引用方向錯」。改框架後同樣 3 個 case、reviewer 滿意。</p>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<p>本卡跟以下三張卡正交、各自處理 case 引用的不同層問題：</p>
<ul>
<li><a href="/blog/report/case-type-graded-citation-depth/" data-link-title="案例引用深度跟著 case 類型走" data-link-desc="skeleton / medium / rich case 各有不同承接深度；誤判類型 → 編造數字 / taxonomy（over-extrapolation）或漏掉 case 揭露的 mechanism（under-citation）；引用前先看 case 行數 &#43; 內容密度判類型、決定該寫『揭露 X 方向』還是『揭露 N 個機制』還是『揭露具體數字 / 設計』">#115 案例引用深度跟著 case 類型走</a> — 處理「case 類型決定引用深度」（skeleton / medium / rich）。本卡處理「case 主軸不對應時的引用框架選擇」、是更上游的問題：先判斷對不對齊、再決定引用深度。</li>
<li><a href="/blog/report/case-citation-three-part-structure/" data-link-title="案例引用三段式段落結構：概念定義 → case 引用 → 通用展開" data-link-desc="Case 引用段落要走三段式結構：(1) 段首概念定義句先寫『該概念是什麼、承擔什麼責任』、(2) 第二位置 case 引用、(3) 通用工程知識展開；段首被 case 引用取代是 06 模組最大宗 systemic 違規（11/12 段都犯）；本卡跟 #115（引用深度）/ #116（內部分層）/ #117（跨 case 合成）正交、處理段落結構順序">#120 案例引用三段式段落結構</a> — 處理「段落結構順序」（概念 → case → 通用展開）。本卡補 #120 的特殊情境：當 case 主軸不對應時、第二段位置的 case 引用該寫什麼。</li>
<li><a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度</a> — 處理「cadence 模板化」。本卡的「強掛」現象背後就是 cadence 模板化的內在動機之一 — 想讓每段都「看起來合規」、結果犧牲語意誠實度。本卡是 #122 在「案例引用」surface 的具體成因 + 修法。</li>
</ul>
<p>跟 <a href="/blog/report/fact-vs-derive-citation-layering/" data-link-title="引用案例要分觀察層 / 判讀層、強化詞是錯位訊號" data-link-desc="引用案例（特別是 rich case）時、case 內容分兩層：觀察層（具體 fact）跟判讀層（作者推論）；兩層在章節引用時要分層標明、避免把作者判讀升級成 case fact；強化詞（才是 / 必須 / 一定 / 關鍵是）通常是錯位訊號、保留 case 原文的條件性表述（取決於 / 核心瓶頸 / 主要驅動）">#116 引用案例要分觀察層 / 判讀層</a> 也有張力：#116 強調觀察層 / 判讀層分明、本卡的反向追問可以視為一種「明示分層」的特殊型 — 把整個引用標為「反向讀法」、相當於把整段都歸到判讀層。</p>
<p>補兩張上位卡：</p>
<ul>
<li><a href="/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">#114 Multi-pass review 的 frame 顆粒度盲點</a> — #146 的「抽象斷言訊號」（「看 X 如何 Y」）就是 #114「keyword bank」機制的具體 keyword 條目。本卡是 #114 機制 1 的應用實例 — 給作者一份可直接 grep 的關鍵字清單。</li>
<li><a href="/blog/report/cross-case-synthesized-frame-must-be-labeled/" data-link-title="跨多個 case 合成的 frame 必須標為章節合成、非 case 原文" data-link-desc="當段落把多個 case 的失效訊號抽象為更高層 frame（如『跨工具回查壓力』『平台責任切分』）、要 explicit 標為『本章合成、非 case 原文』；否則章節 derive 會被讀者當成 case fact、回查 case 時發現章節說的『揭露』實際是章節抽象、不是 case 原文框架">#117 跨多 case 合成的 frame 必須標為章節合成</a> — #117 處理「合成必須明示標示」、本卡的「反向追問」也是明示標示的一種 — 把「我用反向讀法解釋案例」明確告知讀者、避免讀者誤以為 case 直接示範了主題。兩者都處理引用層的誠實標示、是姊妹卡。</li>
</ul>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<p>強掛 case 在以下節點會引爆：</p>
<ul>
<li><strong>Reviewer 第一輪 audit</strong>：fact-check 案例內容 vs 引用描述、不對齊馬上抓出。修正要全段重寫。</li>
<li><strong>讀者回頭追查</strong>：讀者點進 case 看不到引用句宣稱的內容、會懷疑整章其他斷言的可信度。</li>
<li><strong>長期 SSoT 漂移</strong>：案例 case 內容後續更新時、強掛的引用不會跟著更新、變成 stale reference。</li>
</ul>
<p>更深的代價：強掛 case 違反 AGENTS.md 原則八「情境優先於模板」— 把不同案例塞進同一段落模板、抹平案例的真實主軸。Reviewer 抓到的是表面（描述不符）、根因是寫作者讓模板配額凌駕語意誠實度。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>寫完案例段時、用以下訊號自檢、出現任一就考慮切換到反向追問：</p>
<ul>
<li><strong>抽象句型訊號</strong>：引用句寫成「看 X 如何決定 Y」這種無具體斷言的句型、回查 case 找不到「怎麼決定」的具體段落。</li>
<li><strong>句型雷同訊號</strong>：多個 case 引用句型雷同（都是「看 X、對照 Y 段」）、跟 <a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 cadence 同質化</a> 重疊。</li>
<li><strong>維度錯位訊號</strong>：章節主題是 application-layer（query 反模式 / 應用層快取設計）、case 庫主軸是 vendor / 規模 / 容量壓力 — 兩者在不同抽象維度。</li>
<li><strong>配額膨脹訊號</strong>：引用句數 ≥ 3 但每個都「邊際相關」、沒有任一個「直接相關」。</li>
</ul>
<p>四個訊號中出現任一、優先切換到反向追問、別把不對齊強寫成對齊。寫作意圖從「填滿段落」轉成「給讀者誠實證據」、case 段才能撐住 reviewer fact-check。</p>
]]></content:encoded></item><item><title>規範化跟自審是兩種認知任務、立規範當下無法保護同批稿件</title><link>https://tarrragon.github.io/blog/report/rule-codification-vs-self-audit/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/rule-codification-vs-self-audit/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>把反模式抽象成規範卡、跟在自己稿件辨識該反模式的局部實例、是兩種不同認知任務。同一個作者可以清楚寫下「『看 X 如何 Y』是抽象斷言反模式」、同一個 batch 內已寫的 5 篇章節仍能有 11 處該句型未被察覺。&lt;/p>
&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>規範化&lt;/td>
 &lt;td>Outside-in（歸納）&lt;/td>
 &lt;td>找 N 個 case 的共同特徵、命名&lt;/td>
 &lt;td>看到不同 case 重複出現同類問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自審&lt;/td>
 &lt;td>Inside-out（比對）&lt;/td>
 &lt;td>把規範當 grep keyword、掃稿件&lt;/td>
 &lt;td>主動把卡片「判讀徵兆」套到自己文字&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩者用相似概念詞、走不同神經路徑。立規範時注意力放在「為什麼這 pattern 是反模式」、自審時注意力要放在「我這句話符不符合該 pattern」。前一個動作完成、不會自動觸發後一個動作。&lt;/p>
&lt;h2 id="為什麼立規範後仍會犯">為什麼立規範後仍會犯&lt;/h2>
&lt;p>三個認知機制讓兩者解耦：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>抽象化耗用認知頻寬&lt;/strong>：寫下「N+1 query 反模式」這個概念時、作者的工作記憶被 pattern 的本質、對比、邊界佔滿、不會同時掃描自己已寫過的稿件&lt;/li>
&lt;li>&lt;strong>規範化視角是 outside-in&lt;/strong>：規範化把 N 個實例抽象成 1 個模式、看的是「共同特徵」；自審視角是 inside-out、從自己具體句子往外比對、看的是「這句屬不屬於這個 pattern」&lt;/li>
&lt;li>&lt;strong>同 batch 主題語意 attractor&lt;/strong>（見 &lt;a href="https://tarrragon.github.io/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度&lt;/a>）：規範化之前寫的稿件、受同主題 / 同 constraint 拉到相似句型；規範化動作本身不會 retroactive 修這些句型、需要主動 sweep&lt;/li>
&lt;/ol>
&lt;p>這三個機制累積起來、「我剛寫完反模式定義」不等於「我能在自己稿件抓出該反模式的所有實例」。&lt;/p>
&lt;h2 id="case">Case&lt;/h2>
&lt;p>backend 模組 5 篇章節（5.9 / 0.18 / 0.19 / 9.13 / 1.13）的修正過程：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Round 1 reviewer audit&lt;/strong> 抓出 1.13 章節案例引用 mis-cite、修正後寫成 &lt;a href="https://tarrragon.github.io/blog/report/case-misalignment-reverse-inquiry/" data-link-title="案例庫不對齊章節主題時用反向追問取代強掛" data-link-desc="當案例庫主軸跟章節主題不在同一維度時、引用框架要從『正向掛入』切換到『反向追問』；強掛 case 的根因是『想填滿案例段』的模板配額、而非『想讓讀者看到證據』；反向追問把案例庫的限制當 first-class 訊息傳給讀者、case 變成『沒做 X 的後果』的反證、不是 X 的示範；reviewer 第一輪 fact-check 就能抓出強掛、修正成本高；判讀徵兆是引用句寫不出 case 具體段落 / 多個 case 句型雷同 / 章節主題跟 case 庫主軸不同維度">#146 案例庫不對齊章節主題時用反向追問&lt;/a> 卡片。#146 明確列出「抽象斷言訊號：『看 X 如何 Y』這類無具體斷言的句型是反模式」、並作為「判讀徵兆」的四訊號之一。&lt;/li>
&lt;li>&lt;strong>#146 卡寫完當下&lt;/strong>、作者同 batch 已寫的 5 篇章節 case 段內仍有 11 處「看 X 如何 Y」句型未被察覺、未被修正。&lt;/li>
&lt;li>&lt;strong>Round 2 reviewer&lt;/strong> 用 cadence frame 跑 grep（直接拿 #146 描述的反模式當 keyword）、抓出全部 11 處、Round 2 修正後用具體事實 / 數字 / 機制斷言取代。&lt;/li>
&lt;/ol>
&lt;p>這個案例的諷刺感正是本卡的核心：作者剛寫完規範、自審能力卻沒同步提升。中間缺的是「規範化 → grep 自審」這條主動觸發路徑。Round 2 reviewer 補上的就是這條路徑、但理想上規範作者自己當下就該做。&lt;/p>
&lt;h2 id="修法">修法&lt;/h2>
&lt;p>三種觸發機制可以接在規範化動作後：&lt;/p>
&lt;h3 id="1-立規範後立刻跑-keyword-grep含同義變體">1. 立規範後立刻跑 keyword grep（含同義變體）&lt;/h3>
&lt;p>把新立的規範轉成 &lt;code>rg&lt;/code> 可掃的 pattern、對所有同 batch（甚至既有）稿件跑一次 grep：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 例：#146 立下後的 keyword grep&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">rg &lt;span class="s2">&amp;#34;看 .{1,20}如何|看 .{1,20}的決|看 .{1,30}的策略|看 .{1,30}的差異&amp;#34;&lt;/span> content/&amp;lt;scope&amp;gt;/
&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 class="c1"># 但同義變體 grep 同樣重要 — Round 3-A 抓出的盲區&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">rg &lt;span class="s2">&amp;#34;展示 .{1,30}效應|展示 .{1,30}邏輯|本案展示|案例展示&amp;#34;&lt;/span> content/&amp;lt;scope&amp;gt;/&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一輪 grep pattern 受作者當下視野侷限、通常只涵蓋規範化時用的字面例子、漏掉同義變體。「規範化第一次落地不可能 catch 所有同義變體」是這條機制的天然限制 — 需要 Round N 持續擴張 keyword bank、跟 &lt;a href="https://tarrragon.github.io/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">#114 multi-pass review 的 frame 顆粒度盲點&lt;/a> 同源。實證：本卡 Round 2 修「看 X 如何 Y」字面層、Round 3-A 仍能 catch 出「展示 X 效應」「展示 X 邏輯」同義變體、證明 path 需要疊代而非一次性動作。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>把反模式抽象成規範卡、跟在自己稿件辨識該反模式的局部實例、是兩種不同認知任務。同一個作者可以清楚寫下「『看 X 如何 Y』是抽象斷言反模式」、同一個 batch 內已寫的 5 篇章節仍能有 11 處該句型未被察覺。</p>
<p>兩個任務對比：</p>
<table>
  <thead>
      <tr>
          <th>認知任務</th>
          <th>視角</th>
          <th>處理動作</th>
          <th>觸發條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>規範化</td>
          <td>Outside-in（歸納）</td>
          <td>找 N 個 case 的共同特徵、命名</td>
          <td>看到不同 case 重複出現同類問題</td>
      </tr>
      <tr>
          <td>自審</td>
          <td>Inside-out（比對）</td>
          <td>把規範當 grep keyword、掃稿件</td>
          <td>主動把卡片「判讀徵兆」套到自己文字</td>
      </tr>
  </tbody>
</table>
<p>兩者用相似概念詞、走不同神經路徑。立規範時注意力放在「為什麼這 pattern 是反模式」、自審時注意力要放在「我這句話符不符合該 pattern」。前一個動作完成、不會自動觸發後一個動作。</p>
<h2 id="為什麼立規範後仍會犯">為什麼立規範後仍會犯</h2>
<p>三個認知機制讓兩者解耦：</p>
<ol>
<li><strong>抽象化耗用認知頻寬</strong>：寫下「N+1 query 反模式」這個概念時、作者的工作記憶被 pattern 的本質、對比、邊界佔滿、不會同時掃描自己已寫過的稿件</li>
<li><strong>規範化視角是 outside-in</strong>：規範化把 N 個實例抽象成 1 個模式、看的是「共同特徵」；自審視角是 inside-out、從自己具體句子往外比對、看的是「這句屬不屬於這個 pattern」</li>
<li><strong>同 batch 主題語意 attractor</strong>（見 <a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度</a>）：規範化之前寫的稿件、受同主題 / 同 constraint 拉到相似句型；規範化動作本身不會 retroactive 修這些句型、需要主動 sweep</li>
</ol>
<p>這三個機制累積起來、「我剛寫完反模式定義」不等於「我能在自己稿件抓出該反模式的所有實例」。</p>
<h2 id="case">Case</h2>
<p>backend 模組 5 篇章節（5.9 / 0.18 / 0.19 / 9.13 / 1.13）的修正過程：</p>
<ol>
<li><strong>Round 1 reviewer audit</strong> 抓出 1.13 章節案例引用 mis-cite、修正後寫成 <a href="/blog/report/case-misalignment-reverse-inquiry/" data-link-title="案例庫不對齊章節主題時用反向追問取代強掛" data-link-desc="當案例庫主軸跟章節主題不在同一維度時、引用框架要從『正向掛入』切換到『反向追問』；強掛 case 的根因是『想填滿案例段』的模板配額、而非『想讓讀者看到證據』；反向追問把案例庫的限制當 first-class 訊息傳給讀者、case 變成『沒做 X 的後果』的反證、不是 X 的示範；reviewer 第一輪 fact-check 就能抓出強掛、修正成本高；判讀徵兆是引用句寫不出 case 具體段落 / 多個 case 句型雷同 / 章節主題跟 case 庫主軸不同維度">#146 案例庫不對齊章節主題時用反向追問</a> 卡片。#146 明確列出「抽象斷言訊號：『看 X 如何 Y』這類無具體斷言的句型是反模式」、並作為「判讀徵兆」的四訊號之一。</li>
<li><strong>#146 卡寫完當下</strong>、作者同 batch 已寫的 5 篇章節 case 段內仍有 11 處「看 X 如何 Y」句型未被察覺、未被修正。</li>
<li><strong>Round 2 reviewer</strong> 用 cadence frame 跑 grep（直接拿 #146 描述的反模式當 keyword）、抓出全部 11 處、Round 2 修正後用具體事實 / 數字 / 機制斷言取代。</li>
</ol>
<p>這個案例的諷刺感正是本卡的核心：作者剛寫完規範、自審能力卻沒同步提升。中間缺的是「規範化 → grep 自審」這條主動觸發路徑。Round 2 reviewer 補上的就是這條路徑、但理想上規範作者自己當下就該做。</p>
<h2 id="修法">修法</h2>
<p>三種觸發機制可以接在規範化動作後：</p>
<h3 id="1-立規範後立刻跑-keyword-grep含同義變體">1. 立規範後立刻跑 keyword grep（含同義變體）</h3>
<p>把新立的規範轉成 <code>rg</code> 可掃的 pattern、對所有同 batch（甚至既有）稿件跑一次 grep：</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"># 例：#146 立下後的 keyword grep</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rg <span class="s2">&#34;看 .{1,20}如何|看 .{1,20}的決|看 .{1,30}的策略|看 .{1,30}的差異&#34;</span> content/&lt;scope&gt;/
</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="c1"># 但同義變體 grep 同樣重要 — Round 3-A 抓出的盲區</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">rg <span class="s2">&#34;展示 .{1,30}效應|展示 .{1,30}邏輯|本案展示|案例展示&#34;</span> content/&lt;scope&gt;/</span></span></code></pre></div><p>第一輪 grep pattern 受作者當下視野侷限、通常只涵蓋規範化時用的字面例子、漏掉同義變體。「規範化第一次落地不可能 catch 所有同義變體」是這條機制的天然限制 — 需要 Round N 持續擴張 keyword bank、跟 <a href="/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">#114 multi-pass review 的 frame 顆粒度盲點</a> 同源。實證：本卡 Round 2 修「看 X 如何 Y」字面層、Round 3-A 仍能 catch 出「展示 X 效應」「展示 X 邏輯」同義變體、證明 path 需要疊代而非一次性動作。</p>
<h3 id="2-結構句型-cadence-sweep補字面-grep-不抓的維度">2. 結構句型 cadence sweep（補字面 grep 不抓的維度）</h3>
<p>字面 keyword grep 抓詞、抓不到結構骨架。常見漏抓的結構句型：</p>
<ul>
<li>「先 X、再 Y、最後 Z」三段平行</li>
<li>「N 個案例可分別對應&hellip;」枚舉式收尾</li>
<li>「對應本章 X 段」反覆出現、構成段末 cadence</li>
</ul>
<p>這類結構違反字面層查不到、要靠 reviewer 跨稿件對讀才能 catch。修法是擴張 #122 cadence 同質化的 grep target、加入「結構骨架」維度：sweep「先看 .{1,30}、(再|接著)」「N 個 .* 分別」「對應本章「.*」段」這類 pattern。</p>
<h3 id="3-把規範卡的判讀徵兆當-self-audit-checklist">3. 把規範卡的「判讀徵兆」當 self-audit checklist</h3>
<p>每張 report 卡的「判讀徵兆」段（如 #146 列的 4 訊號：抽象句型 / 句型雷同 / 維度錯位 / 配額膨脹）就是現成的 self-audit checklist。立規範當下、作者應該主動把這 checklist 套到自己同 batch 稿件 — 而非預設「我剛寫完應該不會犯」。</p>
<h3 id="4-用-reviewer-跑-in-stream-sampling">4. 用 reviewer 跑 in-stream sampling</h3>
<p>如 <a href="/blog/report/emergence-violations-need-in-stream-sampling/" data-link-title="Emergence-class 違規規則化不了、要 stage 0 變體規劃 &#43; stage 內抽樣兩層" data-link-desc="違規分三類（字面 / 結構 / emergence）、enforcement 時機要對應違規類型；字面違規（emoji / 裸 URL）可 regex hook 在 pre-commit 攔、結構違規（章節缺失 / frontmatter）可 linter 攔、emergence 違規（cadence 同質化 / 跨檔語氣漂移）規則化不了、需要 *stage 0 變體規劃（主動設計）* &#43; *stage 內抽樣（被動監測）* 兩層；checkpoint 只是監測工具、若 stage 0 沒準備 variant、被動抽樣不會自動發現 collapse；補 #82 字面 vs 行為的「時機」軸">#124 emergence-class 違規 enforcement 時機</a> 描述、emergence-class 違規（cadence 同質、抽象斷言這類）字面 hook 抓不到、要 reviewer in-stream 才能發現。本案 Round 2 cadence reviewer 是這個機制的應用、Round 3-A 進一步抓出 Round 2 修法的同義變體盲區 — 兩輪 reviewer 用不同 frame 才能完整覆蓋。理想上規範作者自己應該先做前三層（字面 grep + 結構 sweep + checklist）、reviewer 是補位、不是替代。</p>
<p>四種機制按介入點分層：字面 grep 是 keyword 層、結構 sweep 是 cadence 層、checklist 是徵兆層、reviewer 是 frame 層。立規範後四層都跑一次、覆蓋率最完整。單跑字面 grep（如本卡初版只給 keyword pattern）會漏掉結構 cadence 跟同義變體、屬「規範化視角」單軸盲區、要靠後續疊代擴張。</p>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<ul>
<li><a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度</a> — 解釋「為什麼同 batch 會有 systemic 違規」的成因機制（主題語意 attractor）。本卡補完：規範化動作本身無法解這個 attractor、需要主動 sweep 才能切斷。</li>
<li><a href="/blog/report/emergence-violations-need-in-stream-sampling/" data-link-title="Emergence-class 違規規則化不了、要 stage 0 變體規劃 &#43; stage 內抽樣兩層" data-link-desc="違規分三類（字面 / 結構 / emergence）、enforcement 時機要對應違規類型；字面違規（emoji / 裸 URL）可 regex hook 在 pre-commit 攔、結構違規（章節缺失 / frontmatter）可 linter 攔、emergence 違規（cadence 同質化 / 跨檔語氣漂移）規則化不了、需要 *stage 0 變體規劃（主動設計）* &#43; *stage 內抽樣（被動監測）* 兩層；checkpoint 只是監測工具、若 stage 0 沒準備 variant、被動抽樣不會自動發現 collapse；補 #82 字面 vs 行為的「時機」軸">#124 Emergence-class 違規規則化不了、要 stage 內抽樣</a> — 解釋「什麼時候 enforcement 最有效」（batch 進度 10-20%）。本卡補一個更早的時機點：立規範當下立刻 sweep 同 batch、不必等 batch 進度推進。</li>
<li><a href="/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">#114 Multi-pass review 的 frame 顆粒度盲點</a> — 解釋「為什麼同 reviewer 多輪抓不到不同東西」、提出 keyword bank / reader simulation / self-criticism 三機制。本卡是 #114 在「規範作者本人」這個 reviewer 角色的具體實例：作者剛寫完規範、仍需主動換 frame 才能自審。</li>
<li><a href="/blog/report/collapse-is-implicit-default/" data-link-title="Collapse 是隱形預設：多維空間被壓成單格的三類典型" data-link-desc="決策對話、決策呈現、批量輸出三個 surface 都有同一個 pattern — 高維選擇空間預設被 collapse 到 1-2 個窄格、且這個 collapse 因為「便利 / 合規 / 簡潔」被當成中性預設、不被覺察；#80 是 decision surface 上的極致 collapse、#79 是 dialogue 五維 collapse、#123 是 output framing 在 constraint 下 collapse；三者共骨：*某個高自由度空間被便利驅動 reduce 到最少格子*；對策不是去除 collapse、是讓 collapse 變顯性、由設計者決定要 collapse 哪一維、不是預設全 collapse">#125 Collapse 是隱形預設</a> — 「規範化耗光認知頻寬、自審視角沒上線」是 collapse 的具體機制：規範化動作把高維注意力 reduce 到單軸（pattern 共同特徵）、其他軸（同 batch 自己稿件）被預設關閉。本卡是 #125 在「規範化 surface」的子實例。</li>
<li><a href="/blog/report/writing-review-multi-axis-completeness/" data-link-title="寫作 review 是多軸完整性、不是單軸深度" data-link-desc="寫作 review 的完整性不是單一軸越做越深、是多軸交集都對齊；#83 frame 軸 &#43; #121 instance 軸 &#43; #97 surface 軸 &#43; #95 scope 軸 &#43; #122 cadence 軸 &#43; #124 timing 軸 &#43; #114 granularity 軸、七軸正交、缺任一軸都會 systematic miss；review 設計時要 enumerate 七軸覆蓋狀況、不是只跑一兩個維度做深；是 #79 五維決策對話在 review 工具設計的姊妹卡">#126 寫作 review 是多軸完整性、不是單軸深度</a> — 本卡揭露的「規範化 ≠ 自審」是 #126「Timing 軸」+「Granularity 軸」的具體交集：立規範當下不掃 = timing collapse、規範字面 vs 同 batch 違規 = granularity collapse。</li>
<li><a href="/blog/report/case-misalignment-reverse-inquiry/" data-link-title="案例庫不對齊章節主題時用反向追問取代強掛" data-link-desc="當案例庫主軸跟章節主題不在同一維度時、引用框架要從『正向掛入』切換到『反向追問』；強掛 case 的根因是『想填滿案例段』的模板配額、而非『想讓讀者看到證據』；反向追問把案例庫的限制當 first-class 訊息傳給讀者、case 變成『沒做 X 的後果』的反證、不是 X 的示範；reviewer 第一輪 fact-check 就能抓出強掛、修正成本高；判讀徵兆是引用句寫不出 case 具體段落 / 多個 case 句型雷同 / 章節主題跟 case 庫主軸不同維度">#146 案例庫不對齊章節主題時用反向追問取代強掛</a> — 本卡的 case 來源。#146 才剛立規範、同 batch 仍犯該規範、是「規範化 ≠ 自審」最直接的諷刺證據。本卡跟 #146 互為驗證關係：#146 給出規範本身、本卡解釋為什麼立完規範還需要主動 sweep。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>立規範後若不主動 sweep 同 batch、會出現以下訊號：</p>
<ul>
<li><strong>諷刺對映訊號</strong>：規範卡描述的「反模式」可以一字不改貼回自己稿件、自己仍意識不到。最強訊號、出現代表 inside-out 視角完全沒啟動。</li>
<li><strong>跨稿件 catch 訊號</strong>：該規範立下後一週內 reviewer audit 跨稿件、catch 出該規範的多處違規（≥ 3 處）。代表規範化跟自審之間斷層。</li>
<li><strong>自審盲區訊號</strong>：自己 review 自己稿件時、卡片描述跟稿件實例之間的「相似度」感官弱（明明 textbook 案例、自己讀不出來）。代表規範化耗光認知頻寬、自審視角沒上線。</li>
<li><strong>品質非單調訊號</strong>：同 batch 多篇文章在規範化前後寫的、品質沒有顯著差異。代表規範化未轉換成執行力。</li>
</ul>
<p>出現任一訊號、表示「規範化 → 自審」這條路徑沒接通。立刻跑修法的三層機制（grep / checklist / reviewer）對自己稿件做 sweep。</p>
]]></content:encoded></item><item><title>跨輪 review 停止訊號是 frame 涵蓋、不是 finding 數遞減</title><link>https://tarrragon.github.io/blog/report/cross-round-review-stopping-signal/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cross-round-review-stopping-signal/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>判斷「該不該再來一輪 review」的訊號是「frame 軸是否還有未動」、不是「上一輪 finding 變少」。&lt;/p>
&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>Finding 數遞減&lt;/td>
 &lt;td>Round N 比 Round N-1 finding 少 → 邊際遞減 → 停&lt;/td>
 &lt;td>finding 數明顯下降&lt;/td>
 &lt;td>用錯訊號 — 多輪 review 通常 finding 不遞減&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Frame 涵蓋&lt;/td>
 &lt;td>想不出能 catch 新東西的新 frame → 停&lt;/td>
 &lt;td>七軸（frame / instance / surface / scope / cadence / timing / granularity）全動完&lt;/td>
 &lt;td>需要主動規劃 frame、不是 reactive 判讀&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多輪 review 的 ROI 不是 monotonically decreasing。每輪用新 frame 通常仍會抓出 substantial finding、但內容會從 surface compliance（編號 / 連結 / 案例對應）往深層 structural issue（cadence / enumeration / 反向引用斷裂）走。停止訊號是「下一輪可用的新 frame 已經想不出來」、不是「上一輪 finding 變少」。&lt;/p>
&lt;h2 id="為什麼-finding-數不是停止訊號">為什麼 finding 數不是停止訊號&lt;/h2>
&lt;p>三個原因讓「finding 遞減」誤導：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>每輪修法會 surface 下一輪問題&lt;/strong>：修 cadence 1.0 會把 cadence 從位置 X 漂到位置 Y、變成 cadence 2.0；修 enumeration 不窮盡會 surface 反向引用斷裂（補完 enumeration 才看見哪些章節該引）。修 = 暴露 new surface。&lt;/li>
&lt;li>&lt;strong>frame 切換等於進入新的問題空間&lt;/strong>：Round 1 用 compliance frame catch 不到 cadence 同質化、Round 2 用 cadence frame catch 不到 enumeration 不窮盡、Round 3 用 steelman frame catch 不到 outbound impact。三輪 frame 正交、finding 互不重疊、自然不會遞減。&lt;/li>
&lt;li>&lt;strong>finding 深度遞增、不是寬度遞減&lt;/strong>：Round N 通常需要 frame 更精緻才能 catch、但 catch 到的問題更接近本質。Raw count 可能不變或增加、但每個 finding 的修正成本跟價值都更高。&lt;/li>
&lt;/ol>
&lt;p>把 finding 遞減當停止訊號、會在「正在進入更深層 issue」的時刻錯誤收尾。&lt;/p>
&lt;h2 id="跨輪-review-的質性-transition-模式">跨輪 review 的質性 transition 模式&lt;/h2>
&lt;p>實證觀察、跨輪 review 的 finding 內容會走以下 transition：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>主要 frame&lt;/th>
 &lt;th>finding 性質&lt;/th>
 &lt;th>修法成本&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Surface&lt;/td>
 &lt;td>Compliance / fact-check&lt;/td>
 &lt;td>編號、連結、案例對應、規範違反&lt;/td>
 &lt;td>低（機械修）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cadence&lt;/td>
 &lt;td>字句層 / 模板偵測&lt;/td>
 &lt;td>句型骨架同骨、廢話前綴、地區漂移&lt;/td>
 &lt;td>中（重寫局部）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Structural&lt;/td>
 &lt;td>Steelman / 讀者旅程&lt;/td>
 &lt;td>enumeration 不窮盡、稻草人、反向引用斷裂&lt;/td>
 &lt;td>高（補實質內容）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta&lt;/td>
 &lt;td>Self-application&lt;/td>
 &lt;td>規則自審、同義變體、frame 切換規劃&lt;/td>
 &lt;td>中（疊代擴張）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實證的階段不一定按此順序、但通常從 surface 開始、隨 frame 切換往深層走。Meta 階段在 surface / cadence / structural 都修完後仍能 surface 新問題 — 因為它檢查的是「修法過程本身」、屬另一個維度。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>判斷「該不該再來一輪 review」的訊號是「frame 軸是否還有未動」、不是「上一輪 finding 變少」。</p>
<p>兩種訊號的對比：</p>
<table>
  <thead>
      <tr>
          <th>訊號軸</th>
          <th>判讀方式</th>
          <th>何時觸發停止</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Finding 數遞減</td>
          <td>Round N 比 Round N-1 finding 少 → 邊際遞減 → 停</td>
          <td>finding 數明顯下降</td>
          <td>用錯訊號 — 多輪 review 通常 finding 不遞減</td>
      </tr>
      <tr>
          <td>Frame 涵蓋</td>
          <td>想不出能 catch 新東西的新 frame → 停</td>
          <td>七軸（frame / instance / surface / scope / cadence / timing / granularity）全動完</td>
          <td>需要主動規劃 frame、不是 reactive 判讀</td>
      </tr>
  </tbody>
</table>
<p>多輪 review 的 ROI 不是 monotonically decreasing。每輪用新 frame 通常仍會抓出 substantial finding、但內容會從 surface compliance（編號 / 連結 / 案例對應）往深層 structural issue（cadence / enumeration / 反向引用斷裂）走。停止訊號是「下一輪可用的新 frame 已經想不出來」、不是「上一輪 finding 變少」。</p>
<h2 id="為什麼-finding-數不是停止訊號">為什麼 finding 數不是停止訊號</h2>
<p>三個原因讓「finding 遞減」誤導：</p>
<ol>
<li><strong>每輪修法會 surface 下一輪問題</strong>：修 cadence 1.0 會把 cadence 從位置 X 漂到位置 Y、變成 cadence 2.0；修 enumeration 不窮盡會 surface 反向引用斷裂（補完 enumeration 才看見哪些章節該引）。修 = 暴露 new surface。</li>
<li><strong>frame 切換等於進入新的問題空間</strong>：Round 1 用 compliance frame catch 不到 cadence 同質化、Round 2 用 cadence frame catch 不到 enumeration 不窮盡、Round 3 用 steelman frame catch 不到 outbound impact。三輪 frame 正交、finding 互不重疊、自然不會遞減。</li>
<li><strong>finding 深度遞增、不是寬度遞減</strong>：Round N 通常需要 frame 更精緻才能 catch、但 catch 到的問題更接近本質。Raw count 可能不變或增加、但每個 finding 的修正成本跟價值都更高。</li>
</ol>
<p>把 finding 遞減當停止訊號、會在「正在進入更深層 issue」的時刻錯誤收尾。</p>
<h2 id="跨輪-review-的質性-transition-模式">跨輪 review 的質性 transition 模式</h2>
<p>實證觀察、跨輪 review 的 finding 內容會走以下 transition：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>主要 frame</th>
          <th>finding 性質</th>
          <th>修法成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Surface</td>
          <td>Compliance / fact-check</td>
          <td>編號、連結、案例對應、規範違反</td>
          <td>低（機械修）</td>
      </tr>
      <tr>
          <td>Cadence</td>
          <td>字句層 / 模板偵測</td>
          <td>句型骨架同骨、廢話前綴、地區漂移</td>
          <td>中（重寫局部）</td>
      </tr>
      <tr>
          <td>Structural</td>
          <td>Steelman / 讀者旅程</td>
          <td>enumeration 不窮盡、稻草人、反向引用斷裂</td>
          <td>高（補實質內容）</td>
      </tr>
      <tr>
          <td>Meta</td>
          <td>Self-application</td>
          <td>規則自審、同義變體、frame 切換規劃</td>
          <td>中（疊代擴張）</td>
      </tr>
  </tbody>
</table>
<p>實證的階段不一定按此順序、但通常從 surface 開始、隨 frame 切換往深層走。Meta 階段在 surface / cadence / structural 都修完後仍能 surface 新問題 — 因為它檢查的是「修法過程本身」、屬另一個維度。</p>
<p>每個階段內、frame 用完就遞減；跨階段、新 frame 上線就重新進入「新一輪不遞減」狀態。</p>
<h2 id="停止訊號的-4-個判讀">停止訊號的 4 個判讀</h2>
<p>何時可以判定「真的夠了」？四個判讀齊備、再停：</p>
<ol>
<li><strong>七軸 frame 全動完</strong>：per <a href="/blog/report/writing-review-multi-axis-completeness/" data-link-title="寫作 review 是多軸完整性、不是單軸深度" data-link-desc="寫作 review 的完整性不是單一軸越做越深、是多軸交集都對齊；#83 frame 軸 &#43; #121 instance 軸 &#43; #97 surface 軸 &#43; #95 scope 軸 &#43; #122 cadence 軸 &#43; #124 timing 軸 &#43; #114 granularity 軸、七軸正交、缺任一軸都會 systematic miss；review 設計時要 enumerate 七軸覆蓋狀況、不是只跑一兩個維度做深；是 #79 五維決策對話在 review 工具設計的姊妹卡">#126 review 七軸</a>、frame / instance / surface / scope / cadence / timing / granularity 七軸都用過、沒有遺漏的觀察維度</li>
<li><strong>新 frame 想不出來</strong>：團隊腦力激盪後想不出「能 catch 上一輪 frame 抓不到的東西」的新 frame、代表問題空間已涵蓋</li>
<li><strong>Finding 性質回到 surface</strong>：若新 frame catch 到的 finding 又退回到 surface（編號、連結、低密度 cadence）、代表 structural / meta 維度已穩定</li>
<li><strong>修法成本反轉</strong>：若修一個 finding 的成本超過讀者實際感受的價值、繼續修不划算 — 用 <a href="/blog/report/collapse-is-implicit-default/" data-link-title="Collapse 是隱形預設：多維空間被壓成單格的三類典型" data-link-desc="決策對話、決策呈現、批量輸出三個 surface 都有同一個 pattern — 高維選擇空間預設被 collapse 到 1-2 個窄格、且這個 collapse 因為「便利 / 合規 / 簡潔」被當成中性預設、不被覺察；#80 是 decision surface 上的極致 collapse、#79 是 dialogue 五維 collapse、#123 是 output framing 在 constraint 下 collapse；三者共骨：*某個高自由度空間被便利驅動 reduce 到最少格子*；對策不是去除 collapse、是讓 collapse 變顯性、由設計者決定要 collapse 哪一維、不是預設全 collapse">#125 collapse</a> 的提醒、避免完美主義 collapse 到無止境疊代</li>
</ol>
<p>四個訊號齊備、停的判讀是 evidence-based 而非 finding 數驅動。</p>
<h2 id="case">Case</h2>
<p>本次 backend 5 章 + 1 report 卡的 3 輪 review 實證：</p>
<ul>
<li><strong>Round 1</strong>（compliance / 案例 / 跨章 frame）：12 個 finding、surface 層為主、編號 mis-cite + case mis-citation</li>
<li><strong>Round 2</strong>（cadence / 旅程 / title frame）：10 個 finding、cadence 同骨化 + 影片詞彙橋斷裂 + 時序總表缺失</li>
<li><strong>Round 3</strong>（self-application / steelman / outbound frame）：<strong>16 個 finding</strong>（比 Round 1 / 2 還多）、三段式 cadence 從位置漂移 + enumeration 稻草人 + 單向反向引用斷裂</li>
</ul>
<p>Total 38 個 finding、9 個 reviewer instance、零重疊。Round 3 finding 數反而比 Round 1 / 2 多、但 Round 3 是 review 自然停下的點 — 因為「想不出能 catch Round 3 frame 抓不到的東西的 Round 4 frame」。</p>
<p>判讀停止的依據是 frame 涵蓋（七軸動完、Round 4 frame 想不出來），不是 finding 數遞減（Round 3 數還在升）。若按 finding 遞減判讀、Round 1 → Round 2（12 → 10）就該停、會錯過 Round 3 抓出的 16 個結構性問題。</p>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<p>本卡跟以下卡片正交、處理「多輪 review 何時停」這個 #114 / #126 / #147 沒覆蓋的問題：</p>
<ul>
<li><a href="/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">#114 Multi-pass review 的 frame 顆粒度盲點</a> — 說明「需要不同 frame」。本卡補完：知道需要不同 frame 後、判讀「何時 frame 涵蓋夠」的訊號。</li>
<li><a href="/blog/report/writing-review-multi-axis-completeness/" data-link-title="寫作 review 是多軸完整性、不是單軸深度" data-link-desc="寫作 review 的完整性不是單一軸越做越深、是多軸交集都對齊；#83 frame 軸 &#43; #121 instance 軸 &#43; #97 surface 軸 &#43; #95 scope 軸 &#43; #122 cadence 軸 &#43; #124 timing 軸 &#43; #114 granularity 軸、七軸正交、缺任一軸都會 systematic miss；review 設計時要 enumerate 七軸覆蓋狀況、不是只跑一兩個維度做深；是 #79 五維決策對話在 review 工具設計的姊妹卡">#126 寫作 review 是多軸完整性、不是單軸深度</a> — 列七軸。本卡用七軸作為停止判讀的具體 checklist、補強 #126 在「執行收尾」這層的判讀工具。</li>
<li><a href="/blog/report/rule-codification-vs-self-audit/" data-link-title="規範化跟自審是兩種認知任務、立規範當下無法保護同批稿件" data-link-desc="把反模式抽象成規範卡、跟在自己稿件辨識該反模式的局部實例、是兩種不同認知任務；前者用『歸納共同特徵』的視角、後者用『局部 pattern matching』的視角；用相同概念詞、走不同神經路徑；案例：#146 卡描述「看 X 如何 Y」是反模式、同 batch 5 篇章節仍有 11 處該句型未被作者察覺；修法是規範化當下立刻把規範轉成 grep keyword、對同 batch 稿件主動 sweep；不修則 #122 主題語意 attractor 跟 #124 emergence 違規會在同 batch 內持續累積">#147 規範化跟自審是兩種認知任務</a> — 說明「規範化第一次落地不可能完整、需要疊代」。本卡補完：疊代到什麼時候停？停止訊號跟疊代啟動訊號是不同維度。</li>
<li><a href="/blog/report/collapse-is-implicit-default/" data-link-title="Collapse 是隱形預設：多維空間被壓成單格的三類典型" data-link-desc="決策對話、決策呈現、批量輸出三個 surface 都有同一個 pattern — 高維選擇空間預設被 collapse 到 1-2 個窄格、且這個 collapse 因為「便利 / 合規 / 簡潔」被當成中性預設、不被覺察；#80 是 decision surface 上的極致 collapse、#79 是 dialogue 五維 collapse、#123 是 output framing 在 constraint 下 collapse；三者共骨：*某個高自由度空間被便利驅動 reduce 到最少格子*；對策不是去除 collapse、是讓 collapse 變顯性、由設計者決定要 collapse 哪一維、不是預設全 collapse">#125 Collapse 是隱形預設</a> — 「無止境疊代」是 collapse 的另一個極端（從「規範化單軸 collapse」反向到「review 過度 collapse 完美主義」）。本卡用「修法成本反轉」訊號避免這個反向 collapse。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>跨輪 review 中、出現以下訊號時要重新評估「該繼續還是該停」：</p>
<ul>
<li><strong>新 frame 卡住訊號</strong>：規劃下一輪 review 時、想了 30 分鐘想不出「能 catch 新東西的 frame」— 是「frame 涵蓋已足」的強訊號</li>
<li><strong>Finding 性質退化訊號</strong>：新一輪 finding 退回 surface 層（編號 / 連結這類低密度議題）、structural / meta 層沒新東西 — 代表深層 issue 已穩定</li>
<li><strong>修法成本超過邊際價值訊號</strong>：修一個 finding 要動 50+ 行、但讀者實際感受改善有限 — 修法 ROI 已下降</li>
<li><strong>Frame 重複訊號</strong>：新一輪 reviewer 的 finding 跟上一輪有重疊（per <a href="/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">#114</a> 同 frame 多輪 catch 高度相同）— 代表 frame 軸沒換、再跑無增益</li>
</ul>
<p>四個訊號中出現任二、可以判定「真的夠了」。出現任一、繼續但要規劃 frame 切換。沒有任一、按七軸繼續推進。</p>
<p>「夠了」的判讀本身是 evidence-based、不是直覺 — 用上面四個訊號當 checklist、比「finding 變少就停」可靠。</p>
]]></content:encoded></item><item><title>字句層 review：keyword bank 命中是候選、不是判決</title><link>https://tarrragon.github.io/blog/report/keyword-bank-hit-is-candidate-not-verdict/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/keyword-bank-hit-is-candidate-not-verdict/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>字句層 review 有兩個獨立步驟：偵測（這句有沒有命中可疑訊號）跟判定（這個命中是不是違規）。keyword bank 解的是偵測、判定仍是一個獨立的語意認知步驟。&lt;/strong> reviewer 拿到 grep 命中後、傾向把它合理化成「這個 case 可以接受」而放行 — 偵測成功、判定失敗、違規一樣留在稿件裡。&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>grep keyword bank（#114）&lt;/td>
 &lt;td>關鍵詞不在 bank 裡 → 漏命中（coverage gap）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>判定&lt;/td>
 &lt;td>reviewer 語意判斷&lt;/td>
 &lt;td>命中了、但被合理化成「可接受對照」→ 放行（judgment gap）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>#114 把字句層 review 的失敗歸到偵測層（沒 keyword bank、靠記憶 sweep）。本卡補它沒覆蓋的另一層：&lt;strong>就算 grep 命中、reviewer 仍可能判錯&lt;/strong>。命中只是候選、不是判決。&lt;/p>
&lt;hr>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>寫作規範「正向陳述優先」（主要敘述用正向句建立概念、反例只做對照）的 review、跑 grep &lt;code>不[行可是要能]|無法|沒[做有]|而非|而不是&lt;/code>。這個 bank 會命中兩類完全不同的句子：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>建立概念的否定&lt;/strong>：「高階函式&lt;strong>不是&lt;/strong>『用了比較高級』、&lt;strong>而是&lt;/strong>特定場景的自然解」 — 用否定當核心概念的開場、違反正向陳述。&lt;/li>
&lt;li>&lt;strong>反例對照的否定&lt;/strong>：「對照反例：假設一個只有單一布林設定的 controller…既&lt;strong>沒有&lt;/strong>共用流程、也&lt;strong>沒有&lt;/strong>開放的變化點」 — 在明示的反例段落裡、否定是對照本身、規範允許。&lt;/li>
&lt;/ul>
&lt;p>兩者 grep 命中長得一樣、語意角色相反。reviewer 掃命中清單時、若用「這裡有對照意味、應該算反例」的寬鬆預設、會把第一類也放行 —— 這正是本卡的 case：grep 命中了「不是…而是」、被判成「正向對照修辭、OK」、由讀者再次 feedback 才 catch。&lt;/p>
&lt;p>另有一類問題連命中都沒有：&lt;strong>訴諸群體的安撫贅語&lt;/strong>（「很多人卡在…」「大家都會搞混…」）。這類句子沒有固定關鍵詞（「很多人 / 大家 / 不少開發者 / 初學時」表面形式發散）、keyword bank 結構上抓不到、只能靠語意 pass。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="否定句構命中後用概念位置判定不用有沒有對照意味判定">否定句構命中後、用「概念位置」判定、不用「有沒有對照意味」判定&lt;/h3>
&lt;p>判別問題從「這句有對照意味嗎」（太寬、幾乎都 yes）換成 &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>直接陳述「B 是什麼」、把對 A 的否定整個拿掉&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>明示的「反例 / 對照 / 不適合場景」段落內&lt;/td>
 &lt;td>保留 — 否定是對照本體&lt;/td>
 &lt;td>不動（規範允許、見 #94）&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;strong>正向直述 B&lt;/strong>：「X 不是 A、而是 B」→「X 是 B」、把 A 的排除留給上下文或反例段。把「不是 A」翻成「而非 A」只是換個負向詞、仍違規。&lt;/p>
&lt;p>這條判定準則剛好夾在兩張既有卡之間：#94 警告「別過度刪對照（刪掉承擔 reasoning 的 Y 會讓結論空降）」、正向陳述優先要求「別保留建立概念的否定」。判別線就是上表的「概念位置」—— 概念開場的否定該刪、反例段落的對照該留。&lt;/p>
&lt;h3 id="沒有關鍵詞的贅語靠-reader-simulation-補">沒有關鍵詞的贅語、靠 reader-simulation 補&lt;/h3>
&lt;p>訴諸群體的安撫贅語抓不到固定關鍵詞、改用「換視角」pass：假裝讀者、問每一句「這句給了新資訊、還是只在安撫我（讓我覺得不是只有自己不會）」。教學文的責任是把概念講清楚、不是安撫讀者情緒 —— 安撫句一律刪、保留有教學價值的聚焦（「要分清什麼」）。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="規範掃描器顯示乾淨違規仍在">規範掃描器顯示「乾淨」、違規仍在&lt;/h3>
&lt;p>跑完 grep、把命中逐條判成「可接受」、回報「字句層 clean」 —— 這個 clean 是 judgment 放水的結果、不是真的沒違規。比「沒跑 grep」更危險：沒跑還知道沒查、跑完誤判會產生「已經查過」的虛假信心。&lt;/p>
&lt;h3 id="違規類型有系統性偏移">違規類型有系統性偏移&lt;/h3>
&lt;p>被合理化放行的是&lt;strong>特定一類&lt;/strong>（建立概念的否定句構），有系統性偏移。同一個寬鬆預設會在每篇文章放行同一類違規 —— 跨稿件累積成 systematic miss、跟 #114 的偵測層 systematic miss 同構、只是發生在判定層。&lt;/p>
&lt;h3 id="keyword-bank-越長judgment-放水越隱形">keyword bank 越長、judgment 放水越隱形&lt;/h3>
&lt;p>bank 補得越完整、命中越多、reviewer 越依賴「快速掃過命中清單」、每條停留判定的時間越短、越容易用寬鬆預設批次放行。偵測能力提升反而稀釋判定品質 —— 兩層要分開要求、不能假設「有 bank 就會判對」。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點&lt;/a>&lt;/strong>：#114 解偵測層（把規則展開成 keyword bank、不靠記憶 sweep）。本卡是它的下一層 —— &lt;strong>就算 bank 命中、判定仍可能放水&lt;/strong>。#114 的 keyword bank 範例已收「不是 A 而是 B」、但收進 bank 只保證命中、不保證判對；本卡補「命中後怎麼判」。訴諸群體贅語則是 #114 bank 該補的新 coverage（無固定關鍵詞、靠 reader-simulation）。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據、不能空降結論&lt;/a>&lt;/strong>：#94 跟本卡是同一判定軸的兩極。#94 防「過度刪 —— 刪掉承擔 reasoning 的對照 Y、結論變空降」；本卡防「過度留 —— 保留建立概念的否定、用『這是對照』當藉口」。兩卡合起來才是完整判定準則：以「概念位置」區分該刪（概念開場）還是該留（反例段落 / reasoning 對照）。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../rule-codification-vs-self-audit/">#147 規範化跟自審是兩種認知任務&lt;/a>&lt;/strong>：#147 講「立了規範 ≠ 能在自己稿件辨識實例」。本卡是更細一層 —— &lt;strong>連 grep 命中（自審的最強形式、已經指到具體句子）都可能因判定放水而失效&lt;/strong>。#147 的三層機制（grep / checklist / reviewer in-stream）裡、本卡聚焦 grep 那層的「命中之後」缺口。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精修&lt;/a>&lt;/strong>：grep 命中是字面層攔截、看不到那個否定承擔的是「建立概念」還是「反例對照」 —— 需要 behavioral pass（讀者讀到這句、是拿到正向概念還是被否定句卡住）才能判。本卡是 #82 在「字句層 review 判定」的具體實例。&lt;/li>
&lt;/ul>
&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>grep 命中清單掃過、大半判「可接受對照」&lt;/td>
 &lt;td>停 —— 寬鬆預設正在放水、逐條改用「概念位置」判定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>命中的否定句在段首 / 小節開場 / 核心議題句&lt;/td>
 &lt;td>違規 —— 改正向直述、別翻成「而非」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>把「不是 A 而是 B」改成「而非 A」就收工&lt;/td>
 &lt;td>沒修 —— 只是換個負向詞、仍是否定建立概念&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回報「字句層 clean」但只跑了 grep、沒做語意 pass&lt;/td>
 &lt;td>clean 可能是判定放水 —— 補 reader-simulation 一輪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>句子在安撫讀者（「很多人 / 大家都會」）卻無關鍵詞命中&lt;/td>
 &lt;td>keyword bank 抓不到 —— 靠「這句給新資訊還是安撫」語意問句刪&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：正向陳述優先這類「同一訊號命中、語意角色相反」的字句層規範 review；AI 輔助寫作的 self-review（最容易在判定層放水）；keyword bank 已建立、但 review 回報品質仍不穩的情境。&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：偵測本身就漏（關鍵詞不在 bank）的問題 —— 那是 #114 的偵測層、先補 bank 再談判定。&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：判定收緊 ≠ 一律刪否定。明示反例段落的對照（#94 的 reasoning Y）該留；判別線是「概念位置」、不是「有沒有否定詞」。收太緊會退回 #94 警告的「空降斷言」。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源&lt;/h2>
&lt;p>本卡觸發於 review &lt;a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式&lt;/a> 時。流程是先跑 3 個 frame 的 multi-round review + 字句層 grep keyword bank：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>字句層 review 有兩個獨立步驟：偵測（這句有沒有命中可疑訊號）跟判定（這個命中是不是違規）。keyword bank 解的是偵測、判定仍是一個獨立的語意認知步驟。</strong> reviewer 拿到 grep 命中後、傾向把它合理化成「這個 case 可以接受」而放行 — 偵測成功、判定失敗、違規一樣留在稿件裡。</p>
<table>
  <thead>
      <tr>
          <th>步驟</th>
          <th>工具</th>
          <th>失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>偵測</td>
          <td>grep keyword bank（#114）</td>
          <td>關鍵詞不在 bank 裡 → 漏命中（coverage gap）</td>
      </tr>
      <tr>
          <td>判定</td>
          <td>reviewer 語意判斷</td>
          <td>命中了、但被合理化成「可接受對照」→ 放行（judgment gap）</td>
      </tr>
  </tbody>
</table>
<p>#114 把字句層 review 的失敗歸到偵測層（沒 keyword bank、靠記憶 sweep）。本卡補它沒覆蓋的另一層：<strong>就算 grep 命中、reviewer 仍可能判錯</strong>。命中只是候選、不是判決。</p>
<hr>
<h2 id="情境">情境</h2>
<p>寫作規範「正向陳述優先」（主要敘述用正向句建立概念、反例只做對照）的 review、跑 grep <code>不[行可是要能]|無法|沒[做有]|而非|而不是</code>。這個 bank 會命中兩類完全不同的句子：</p>
<ul>
<li><strong>建立概念的否定</strong>：「高階函式<strong>不是</strong>『用了比較高級』、<strong>而是</strong>特定場景的自然解」 — 用否定當核心概念的開場、違反正向陳述。</li>
<li><strong>反例對照的否定</strong>：「對照反例：假設一個只有單一布林設定的 controller…既<strong>沒有</strong>共用流程、也<strong>沒有</strong>開放的變化點」 — 在明示的反例段落裡、否定是對照本身、規範允許。</li>
</ul>
<p>兩者 grep 命中長得一樣、語意角色相反。reviewer 掃命中清單時、若用「這裡有對照意味、應該算反例」的寬鬆預設、會把第一類也放行 —— 這正是本卡的 case：grep 命中了「不是…而是」、被判成「正向對照修辭、OK」、由讀者再次 feedback 才 catch。</p>
<p>另有一類問題連命中都沒有：<strong>訴諸群體的安撫贅語</strong>（「很多人卡在…」「大家都會搞混…」）。這類句子沒有固定關鍵詞（「很多人 / 大家 / 不少開發者 / 初學時」表面形式發散）、keyword bank 結構上抓不到、只能靠語意 pass。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="否定句構命中後用概念位置判定不用有沒有對照意味判定">否定句構命中後、用「概念位置」判定、不用「有沒有對照意味」判定</h3>
<p>判別問題從「這句有對照意味嗎」（太寬、幾乎都 yes）換成 <strong>「這個否定在建立核心概念、還是在明示的反例段落裡做對照」</strong>：</p>
<table>
  <thead>
      <tr>
          <th>命中位置</th>
          <th>判定</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>段首 / 小節開場 / 核心議題句</td>
          <td>違規 — 用否定建立概念</td>
          <td>直接陳述「B 是什麼」、把對 A 的否定整個拿掉</td>
      </tr>
      <tr>
          <td>明示的「反例 / 對照 / 不適合場景」段落內</td>
          <td>保留 — 否定是對照本體</td>
          <td>不動（規範允許、見 #94）</td>
      </tr>
      <tr>
          <td>用對比定義術語（「裸寫 = 略過取名」）</td>
          <td>保留 — 否定是定義的本質</td>
          <td>不動</td>
      </tr>
  </tbody>
</table>
<p>關鍵修法是<strong>正向直述 B</strong>：「X 不是 A、而是 B」→「X 是 B」、把 A 的排除留給上下文或反例段。把「不是 A」翻成「而非 A」只是換個負向詞、仍違規。</p>
<p>這條判定準則剛好夾在兩張既有卡之間：#94 警告「別過度刪對照（刪掉承擔 reasoning 的 Y 會讓結論空降）」、正向陳述優先要求「別保留建立概念的否定」。判別線就是上表的「概念位置」—— 概念開場的否定該刪、反例段落的對照該留。</p>
<h3 id="沒有關鍵詞的贅語靠-reader-simulation-補">沒有關鍵詞的贅語、靠 reader-simulation 補</h3>
<p>訴諸群體的安撫贅語抓不到固定關鍵詞、改用「換視角」pass：假裝讀者、問每一句「這句給了新資訊、還是只在安撫我（讓我覺得不是只有自己不會）」。教學文的責任是把概念講清楚、不是安撫讀者情緒 —— 安撫句一律刪、保留有教學價值的聚焦（「要分清什麼」）。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="規範掃描器顯示乾淨違規仍在">規範掃描器顯示「乾淨」、違規仍在</h3>
<p>跑完 grep、把命中逐條判成「可接受」、回報「字句層 clean」 —— 這個 clean 是 judgment 放水的結果、不是真的沒違規。比「沒跑 grep」更危險：沒跑還知道沒查、跑完誤判會產生「已經查過」的虛假信心。</p>
<h3 id="違規類型有系統性偏移">違規類型有系統性偏移</h3>
<p>被合理化放行的是<strong>特定一類</strong>（建立概念的否定句構），有系統性偏移。同一個寬鬆預設會在每篇文章放行同一類違規 —— 跨稿件累積成 systematic miss、跟 #114 的偵測層 systematic miss 同構、只是發生在判定層。</p>
<h3 id="keyword-bank-越長judgment-放水越隱形">keyword bank 越長、judgment 放水越隱形</h3>
<p>bank 補得越完整、命中越多、reviewer 越依賴「快速掃過命中清單」、每條停留判定的時間越短、越容易用寬鬆預設批次放行。偵測能力提升反而稀釋判定品質 —— 兩層要分開要求、不能假設「有 bank 就會判對」。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點</a></strong>：#114 解偵測層（把規則展開成 keyword bank、不靠記憶 sweep）。本卡是它的下一層 —— <strong>就算 bank 命中、判定仍可能放水</strong>。#114 的 keyword bank 範例已收「不是 A 而是 B」、但收進 bank 只保證命中、不保證判對；本卡補「命中後怎麼判」。訴諸群體贅語則是 #114 bank 該補的新 coverage（無固定關鍵詞、靠 reader-simulation）。</li>
<li><strong><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據、不能空降結論</a></strong>：#94 跟本卡是同一判定軸的兩極。#94 防「過度刪 —— 刪掉承擔 reasoning 的對照 Y、結論變空降」；本卡防「過度留 —— 保留建立概念的否定、用『這是對照』當藉口」。兩卡合起來才是完整判定準則：以「概念位置」區分該刪（概念開場）還是該留（反例段落 / reasoning 對照）。</li>
<li><strong><a href="../rule-codification-vs-self-audit/">#147 規範化跟自審是兩種認知任務</a></strong>：#147 講「立了規範 ≠ 能在自己稿件辨識實例」。本卡是更細一層 —— <strong>連 grep 命中（自審的最強形式、已經指到具體句子）都可能因判定放水而失效</strong>。#147 的三層機制（grep / checklist / reviewer in-stream）裡、本卡聚焦 grep 那層的「命中之後」缺口。</li>
<li><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精修</a></strong>：grep 命中是字面層攔截、看不到那個否定承擔的是「建立概念」還是「反例對照」 —— 需要 behavioral pass（讀者讀到這句、是拿到正向概念還是被否定句卡住）才能判。本卡是 #82 在「字句層 review 判定」的具體實例。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>grep 命中清單掃過、大半判「可接受對照」</td>
          <td>停 —— 寬鬆預設正在放水、逐條改用「概念位置」判定</td>
      </tr>
      <tr>
          <td>命中的否定句在段首 / 小節開場 / 核心議題句</td>
          <td>違規 —— 改正向直述、別翻成「而非」</td>
      </tr>
      <tr>
          <td>把「不是 A 而是 B」改成「而非 A」就收工</td>
          <td>沒修 —— 只是換個負向詞、仍是否定建立概念</td>
      </tr>
      <tr>
          <td>回報「字句層 clean」但只跑了 grep、沒做語意 pass</td>
          <td>clean 可能是判定放水 —— 補 reader-simulation 一輪</td>
      </tr>
      <tr>
          <td>句子在安撫讀者（「很多人 / 大家都會」）卻無關鍵詞命中</td>
          <td>keyword bank 抓不到 —— 靠「這句給新資訊還是安撫」語意問句刪</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：正向陳述優先這類「同一訊號命中、語意角色相反」的字句層規範 review；AI 輔助寫作的 self-review（最容易在判定層放水）；keyword bank 已建立、但 review 回報品質仍不穩的情境。</li>
<li><strong>不適用</strong>：偵測本身就漏（關鍵詞不在 bank）的問題 —— 那是 #114 的偵測層、先補 bank 再談判定。</li>
<li><strong>邊界</strong>：判定收緊 ≠ 一律刪否定。明示反例段落的對照（#94 的 reasoning Y）該留；判別線是「概念位置」、不是「有沒有否定詞」。收太緊會退回 #94 警告的「空降斷言」。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於 review <a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式</a> 時。流程是先跑 3 個 frame 的 multi-round review + 字句層 grep keyword bank：</p>
<ol>
<li>grep <strong>命中</strong>了核心議題句「高階函式<strong>不是</strong>『用了比較高級』、<strong>而是</strong>特定場景的自然解」跟小節開場「<strong>不是</strong>『能用就用』、<strong>而是</strong>…三個特徵」。</li>
<li>我把這些命中判成「正向對照修辭、OK」、回報「字句層大致乾淨」。</li>
<li>讀者 feedback 指出這仍違反正向陳述 —— 偵測成功、判定失敗。</li>
<li>修正後讀者再指出「很多人卡在…」是訴諸群體的安撫贅語 —— 這類連 grep 都沒命中（無固定關鍵詞）。</li>
</ol>
<p>對應本卡：<strong>keyword bank 命中是候選、不是判決</strong>。第一類（命中誤判）揭露判定層盲點、第二類（無命中）揭露 coverage 仍需 reader-simulation 補。兩者共同說明：字句層 review 的偵測（grep）跟判定（語意）是兩個步驟、不能假設「跑了 bank 就會判對、就會抓全」。</p>
]]></content:encoded></item><item><title>教材用中性陳述、不對讀者喊話</title><link>https://tarrragon.github.io/blog/report/teaching-register-states-not-addresses-reader/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/teaching-register-states-not-addresses-reader/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>1 個 case&lt;/strong>（&lt;a href="../../work-log/dart_hof_typedef_readability/">HOF / typedef 可讀性文章&lt;/a>的 review、一次抓到 3 種對讀者喊話的形式）抽出來的觀察。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>三 sub-case 是 starting set、不是窮舉&lt;/strong>：安撫 / 第二人稱 / 祈使是這次出現過的、不代表「對讀者喊話的完整形式庫」。新形式（反問句「想想看為什麼？」、預告「接下來你會看到」）出現時要持續擴充。&lt;/li>
&lt;li>&lt;strong>「中性 vs 親近」是情境化取捨、不是 zero-sum&lt;/strong>：本卡聚焦「判斷工具型 / 概念建立段」的喊話；hook / 引言 / narrative 段落的輕度第二人稱反而幫讀者進入論述、不適用本卡。&lt;/li>
&lt;li>&lt;strong>修補有效性未獨立驗證&lt;/strong>：改成中性陳述後讀起來更聚焦、但「register 跟讀者實際理解之間的相關性」沒做使用者測試。&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、先判斷段落是否屬於「概念建立 / 判斷工具型」——是 → 套用；否（hook / narrative）→ 評估親近感跟中性的取捨。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>教材的責任是把概念講清楚、不是管理讀者的情緒或閱讀節奏。&lt;/strong> 對讀者喊話的句子讀起來親近、但承載的是「對話 stance」而非概念內容 —— 把讀者當成要被安撫、被代入、被指揮的對象，多出來的是資訊負擔、拖慢進入正題。&lt;/p>
&lt;p>下面三種形式是觀察到的 pattern —— 不是窮舉、實際形式可能更多：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>對讀者喊話的形式&lt;/th>
 &lt;th>具體案例&lt;/th>
 &lt;th>喊話的 stance&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>直接講概念：「這個簽章的關鍵是分清 X」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第二人稱代入&lt;/td>
 &lt;td>「你天天寫的 &lt;code>int count&lt;/code>」「你天天在用 map」&lt;/td>
 &lt;td>把讀者拉進句子當主詞&lt;/td>
 &lt;td>中性指稱：「常見的 &lt;code>int count&lt;/code>」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>祈使控制閱讀&lt;/td>
 &lt;td>「先讀懂這個簽章」「先釐清 X」「別搞混」&lt;/td>
 &lt;td>指揮讀者的閱讀順序 / 動作&lt;/td>
 &lt;td>描述性名詞標題：「簽章的型別與名字拆解」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵是這三種&lt;strong>精度都沒問題&lt;/strong> —— 「你天天寫的 &lt;code>int count&lt;/code>」指涉完全正確、「先讀懂這個簽章」語意清楚。違反不在用詞精度、在 &lt;strong>register/stance&lt;/strong>：教材該是「陳述概念」、喊話是「對讀者說話」。&lt;/p>
&lt;hr>
&lt;h2 id="三種形式的具體-case">三種形式的具體 case&lt;/h2>
&lt;h3 id="形式-1安撫情緒--訴諸群體">形式 1：安撫情緒 / 訴諸群體&lt;/h3>
&lt;p>&lt;strong>喊話版&lt;/strong>：「很多人卡在『這串到底哪裡是型別、哪裡是名字』。」&lt;/p>
&lt;p>&lt;strong>問題&lt;/strong>：「很多人」沒有資訊量 —— 讀者不會因為「別人也卡」而更懂型別跟名字怎麼分。它的功能是情緒安撫（你不孤單）、不是概念傳遞。教材的價值在講清楚概念、讀者的情緒不是它的責任。&lt;/p>
&lt;p>&lt;strong>中性版&lt;/strong>：「這個簽章的關鍵是分清『哪裡是型別、哪裡是名字』。」&lt;/p>
&lt;p>差別：中性版第一句就是概念聚焦（要分清什麼）、喊話版前半句純安撫、讀者要跳過它才到正題。&lt;/p>
&lt;h3 id="形式-2第二人稱代入">形式 2：第二人稱代入&lt;/h3>
&lt;p>&lt;strong>喊話版&lt;/strong>：「順序跟&lt;strong>你天天寫的&lt;/strong> &lt;code>int count&lt;/code>、&lt;code>Color color&lt;/code> 一樣」&lt;/p>
&lt;p>&lt;strong>問題&lt;/strong>：「你天天寫的」把讀者拉進句子當主詞、預設讀者的日常（萬一讀者沒天天寫 Dart？）。它想營造親近感、但概念（&lt;code>int count&lt;/code> 是 &lt;code>型別 名字&lt;/code> 順序）不依賴「誰寫的」。&lt;/p>
&lt;p>&lt;strong>中性版&lt;/strong>：「順序跟&lt;strong>常見的&lt;/strong> &lt;code>int count&lt;/code>、&lt;code>Color color&lt;/code> 一樣」&lt;/p>
&lt;p>差別：中性版用「常見的」指涉同一組例子、不預設讀者身分、概念一樣到位。&lt;/p>
&lt;h3 id="形式-3祈使控制閱讀">形式 3：祈使控制閱讀&lt;/h3>
&lt;p>&lt;strong>喊話版&lt;/strong>：小節標題「&lt;strong>先讀懂&lt;/strong>這個簽章的每個部分」、「&lt;strong>先釐清&lt;/strong>：什麼是 X」&lt;/p>
&lt;p>&lt;strong>問題&lt;/strong>：「先讀懂 / 先釐清」是對讀者下閱讀指令、控制節奏。標題的責任是標示這段在講什麼（讓讀者自己決定讀不讀）、不是指揮讀者「先做什麼」。&lt;/p>
&lt;p>&lt;strong>中性版&lt;/strong>：描述性名詞標題「簽章的型別與名字拆解」、「什麼是『函式型別裸寫在簽章』」&lt;/p>
&lt;p>差別：名詞標題讓讀者掃目錄就知道內容、祈使標題把目錄變成指令清單。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="資訊負擔累積稀釋概念密度">資訊負擔累積、稀釋概念密度&lt;/h3>
&lt;p>每句喊話都是「非概念」的字 —— 安撫、代入、指令本身不傳遞知識。教材裡散佈喊話、讀者要持續過濾「這句給資訊還是在跟我說話」、概念密度被稀釋。&lt;/p>
&lt;h3 id="預設讀者身分對不上就失效">預設讀者身分、對不上就失效&lt;/h3>
&lt;p>「你天天寫的」「很多人卡在」都預設了讀者的背景 / 經驗 / 困難。對不上的讀者（沒天天寫、第一次就懂）會感到突兀、甚至被排除 —— 跟 AGENTS 原則六「讀者定位用內容體現、不貼標籤」同源的失效。&lt;/p>
&lt;h3 id="keyword-bank-抓不到靠語意-pass">Keyword bank 抓不到、靠語意 pass&lt;/h3>
&lt;p>第二人稱（&lt;code>你&lt;/code> 會誤命中 code 註解）跟祈使（句式發散、無固定詞）沒有穩定關鍵詞、grep keyword bank 結構上抓不到 —— 這類問題只能靠 reader-simulation 語意 pass（「這句在給資訊、還是在指揮 / 安撫讀者？」）。正是 #149 講的 coverage gap。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度&lt;/a>&lt;/strong>：兩卡是字句層的 sibling、但軸不同。#111 是「&lt;strong>精度&lt;/strong>」軸（用詞能不能反推機制 / 條件 / 契約）；本卡是「&lt;strong>register/stance&lt;/strong>」軸（陳述概念 vs 對讀者說話）。「你天天寫的 &lt;code>int count&lt;/code>」精度完全正確、卻是錯 register —— #111 抓不到、要本卡。兩卡共享同一個情境邊界：hook / narrative 段落都放寬。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 keyword bank 命中是候選、不是判決&lt;/a>&lt;/strong>：本卡是 #149 的 content 對偶。#149 是 &lt;strong>review-process&lt;/strong>（偵測≠判定、怎麼審）、本卡是 &lt;strong>content 原則&lt;/strong>（prose 該怎麼寫）。#149 把「安撫贅語」當 coverage-gap 的&lt;strong>舉例&lt;/strong>、本卡把它&lt;strong>確立成 content 原則&lt;/strong>。產生情境不同：#149 源於 reviewer 誤判 grep 命中、本卡源於寫作當下 prose 對讀者喊話。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點&lt;/a>&lt;/strong>：本卡的三形式裡、第二人稱跟祈使&lt;strong>沒有固定關鍵詞&lt;/strong>、是 #114 keyword bank 機制的已知盲區 —— 補強的是 #114 的 reader-simulation 機制（換視角）、不是 keyword bank（換工具）。&lt;/li>
&lt;li>&lt;strong>AGENTS 原則六（讀者定位用內容體現）&lt;/strong>：兩者相鄰但機制不同。原則六禁「&lt;strong>貼標籤&lt;/strong>」（叫讀者『新手 / 新人』）、本卡禁「&lt;strong>稱呼 / 指揮&lt;/strong>」（你 / 先讀懂）。可以不貼標籤卻仍在喊話（「你天天寫的」沒叫讀者新手、仍是第二人稱代入）—— 本卡補原則六沒覆蓋的 stance 維度。&lt;/li>
&lt;/ul>
&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>段落 / 標題出現「很多人 / 大家 / 不少開發者」&lt;/td>
 &lt;td>訴諸群體安撫 —— 刪、改成概念聚焦句&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>概念建立段出現「你 / 你的 / 你會 / 你天天」&lt;/td>
 &lt;td>第二人稱代入 —— 改中性指稱（常見的 / 一般而言）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>標題或開場是「先讀懂 / 先釐清 / 別搞混 / 記住」&lt;/td>
 &lt;td>祈使控制閱讀 —— 改描述性名詞標題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>grep &lt;code>你&lt;/code> 命中、但分不清正文還是 code 註解&lt;/td>
 &lt;td>keyword bank 抓不準 —— 改 reader-simulation 語意問句&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自問「這句給新概念、還是在跟讀者說話」答後者&lt;/td>
 &lt;td>對讀者喊話 —— 刪 stance、留概念&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：概念建立段、判斷工具型段落、小節標題 —— 這些地方的責任是傳遞概念 / 標示內容、喊話純屬負擔。&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：hook / 引言 / narrative 開場 —— 輕度第二人稱（「假設你在維護一個…」）能幫讀者進入情境、跟 #111 的 hook 例外同理。&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：中性化 ≠ 消滅所有「你」。判別線是「這句在&lt;strong>傳遞概念 / 標示內容&lt;/strong>、還是在&lt;strong>管理讀者&lt;/strong>（安撫情緒 / 代入身分 / 指揮閱讀）」—— 後者才刪。protocol 型文件（操作步驟、checklist）的祈使是合理 stance、不適用本卡。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源&lt;/h2>
&lt;p>本卡觸發於 review &lt;a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式&lt;/a> 時、由讀者連續指出三種對讀者喊話：&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>1 個 case</strong>（<a href="../../work-log/dart_hof_typedef_readability/">HOF / typedef 可讀性文章</a>的 review、一次抓到 3 種對讀者喊話的形式）抽出來的觀察。具體限制：</p>
<ul>
<li><strong>三 sub-case 是 starting set、不是窮舉</strong>：安撫 / 第二人稱 / 祈使是這次出現過的、不代表「對讀者喊話的完整形式庫」。新形式（反問句「想想看為什麼？」、預告「接下來你會看到」）出現時要持續擴充。</li>
<li><strong>「中性 vs 親近」是情境化取捨、不是 zero-sum</strong>：本卡聚焦「判斷工具型 / 概念建立段」的喊話；hook / 引言 / narrative 段落的輕度第二人稱反而幫讀者進入論述、不適用本卡。</li>
<li><strong>修補有效性未獨立驗證</strong>：改成中性陳述後讀起來更聚焦、但「register 跟讀者實際理解之間的相關性」沒做使用者測試。</li>
</ul>
<p>讀者使用本卡時、先判斷段落是否屬於「概念建立 / 判斷工具型」——是 → 套用；否（hook / narrative）→ 評估親近感跟中性的取捨。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p><strong>教材的責任是把概念講清楚、不是管理讀者的情緒或閱讀節奏。</strong> 對讀者喊話的句子讀起來親近、但承載的是「對話 stance」而非概念內容 —— 把讀者當成要被安撫、被代入、被指揮的對象，多出來的是資訊負擔、拖慢進入正題。</p>
<p>下面三種形式是觀察到的 pattern —— 不是窮舉、實際形式可能更多：</p>
<table>
  <thead>
      <tr>
          <th>對讀者喊話的形式</th>
          <th>具體案例</th>
          <th>喊話的 stance</th>
          <th>中性改法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>安撫情緒 / 訴諸群體</td>
          <td>「很多人卡在…」「大家都會搞混…」</td>
          <td>讓讀者覺得「不是只有自己不會」</td>
          <td>直接講概念：「這個簽章的關鍵是分清 X」</td>
      </tr>
      <tr>
          <td>第二人稱代入</td>
          <td>「你天天寫的 <code>int count</code>」「你天天在用 map」</td>
          <td>把讀者拉進句子當主詞</td>
          <td>中性指稱：「常見的 <code>int count</code>」</td>
      </tr>
      <tr>
          <td>祈使控制閱讀</td>
          <td>「先讀懂這個簽章」「先釐清 X」「別搞混」</td>
          <td>指揮讀者的閱讀順序 / 動作</td>
          <td>描述性名詞標題：「簽章的型別與名字拆解」</td>
      </tr>
  </tbody>
</table>
<p>關鍵是這三種<strong>精度都沒問題</strong> —— 「你天天寫的 <code>int count</code>」指涉完全正確、「先讀懂這個簽章」語意清楚。違反不在用詞精度、在 <strong>register/stance</strong>：教材該是「陳述概念」、喊話是「對讀者說話」。</p>
<hr>
<h2 id="三種形式的具體-case">三種形式的具體 case</h2>
<h3 id="形式-1安撫情緒--訴諸群體">形式 1：安撫情緒 / 訴諸群體</h3>
<p><strong>喊話版</strong>：「很多人卡在『這串到底哪裡是型別、哪裡是名字』。」</p>
<p><strong>問題</strong>：「很多人」沒有資訊量 —— 讀者不會因為「別人也卡」而更懂型別跟名字怎麼分。它的功能是情緒安撫（你不孤單）、不是概念傳遞。教材的價值在講清楚概念、讀者的情緒不是它的責任。</p>
<p><strong>中性版</strong>：「這個簽章的關鍵是分清『哪裡是型別、哪裡是名字』。」</p>
<p>差別：中性版第一句就是概念聚焦（要分清什麼）、喊話版前半句純安撫、讀者要跳過它才到正題。</p>
<h3 id="形式-2第二人稱代入">形式 2：第二人稱代入</h3>
<p><strong>喊話版</strong>：「順序跟<strong>你天天寫的</strong> <code>int count</code>、<code>Color color</code> 一樣」</p>
<p><strong>問題</strong>：「你天天寫的」把讀者拉進句子當主詞、預設讀者的日常（萬一讀者沒天天寫 Dart？）。它想營造親近感、但概念（<code>int count</code> 是 <code>型別 名字</code> 順序）不依賴「誰寫的」。</p>
<p><strong>中性版</strong>：「順序跟<strong>常見的</strong> <code>int count</code>、<code>Color color</code> 一樣」</p>
<p>差別：中性版用「常見的」指涉同一組例子、不預設讀者身分、概念一樣到位。</p>
<h3 id="形式-3祈使控制閱讀">形式 3：祈使控制閱讀</h3>
<p><strong>喊話版</strong>：小節標題「<strong>先讀懂</strong>這個簽章的每個部分」、「<strong>先釐清</strong>：什麼是 X」</p>
<p><strong>問題</strong>：「先讀懂 / 先釐清」是對讀者下閱讀指令、控制節奏。標題的責任是標示這段在講什麼（讓讀者自己決定讀不讀）、不是指揮讀者「先做什麼」。</p>
<p><strong>中性版</strong>：描述性名詞標題「簽章的型別與名字拆解」、「什麼是『函式型別裸寫在簽章』」</p>
<p>差別：名詞標題讓讀者掃目錄就知道內容、祈使標題把目錄變成指令清單。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="資訊負擔累積稀釋概念密度">資訊負擔累積、稀釋概念密度</h3>
<p>每句喊話都是「非概念」的字 —— 安撫、代入、指令本身不傳遞知識。教材裡散佈喊話、讀者要持續過濾「這句給資訊還是在跟我說話」、概念密度被稀釋。</p>
<h3 id="預設讀者身分對不上就失效">預設讀者身分、對不上就失效</h3>
<p>「你天天寫的」「很多人卡在」都預設了讀者的背景 / 經驗 / 困難。對不上的讀者（沒天天寫、第一次就懂）會感到突兀、甚至被排除 —— 跟 AGENTS 原則六「讀者定位用內容體現、不貼標籤」同源的失效。</p>
<h3 id="keyword-bank-抓不到靠語意-pass">Keyword bank 抓不到、靠語意 pass</h3>
<p>第二人稱（<code>你</code> 會誤命中 code 註解）跟祈使（句式發散、無固定詞）沒有穩定關鍵詞、grep keyword bank 結構上抓不到 —— 這類問題只能靠 reader-simulation 語意 pass（「這句在給資訊、還是在指揮 / 安撫讀者？」）。正是 #149 講的 coverage gap。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度</a></strong>：兩卡是字句層的 sibling、但軸不同。#111 是「<strong>精度</strong>」軸（用詞能不能反推機制 / 條件 / 契約）；本卡是「<strong>register/stance</strong>」軸（陳述概念 vs 對讀者說話）。「你天天寫的 <code>int count</code>」精度完全正確、卻是錯 register —— #111 抓不到、要本卡。兩卡共享同一個情境邊界：hook / narrative 段落都放寬。</li>
<li><strong><a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 keyword bank 命中是候選、不是判決</a></strong>：本卡是 #149 的 content 對偶。#149 是 <strong>review-process</strong>（偵測≠判定、怎麼審）、本卡是 <strong>content 原則</strong>（prose 該怎麼寫）。#149 把「安撫贅語」當 coverage-gap 的<strong>舉例</strong>、本卡把它<strong>確立成 content 原則</strong>。產生情境不同：#149 源於 reviewer 誤判 grep 命中、本卡源於寫作當下 prose 對讀者喊話。</li>
<li><strong><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點</a></strong>：本卡的三形式裡、第二人稱跟祈使<strong>沒有固定關鍵詞</strong>、是 #114 keyword bank 機制的已知盲區 —— 補強的是 #114 的 reader-simulation 機制（換視角）、不是 keyword bank（換工具）。</li>
<li><strong>AGENTS 原則六（讀者定位用內容體現）</strong>：兩者相鄰但機制不同。原則六禁「<strong>貼標籤</strong>」（叫讀者『新手 / 新人』）、本卡禁「<strong>稱呼 / 指揮</strong>」（你 / 先讀懂）。可以不貼標籤卻仍在喊話（「你天天寫的」沒叫讀者新手、仍是第二人稱代入）—— 本卡補原則六沒覆蓋的 stance 維度。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<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>grep <code>你</code> 命中、但分不清正文還是 code 註解</td>
          <td>keyword bank 抓不準 —— 改 reader-simulation 語意問句</td>
      </tr>
      <tr>
          <td>自問「這句給新概念、還是在跟讀者說話」答後者</td>
          <td>對讀者喊話 —— 刪 stance、留概念</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：概念建立段、判斷工具型段落、小節標題 —— 這些地方的責任是傳遞概念 / 標示內容、喊話純屬負擔。</li>
<li><strong>不適用</strong>：hook / 引言 / narrative 開場 —— 輕度第二人稱（「假設你在維護一個…」）能幫讀者進入情境、跟 #111 的 hook 例外同理。</li>
<li><strong>邊界</strong>：中性化 ≠ 消滅所有「你」。判別線是「這句在<strong>傳遞概念 / 標示內容</strong>、還是在<strong>管理讀者</strong>（安撫情緒 / 代入身分 / 指揮閱讀）」—— 後者才刪。protocol 型文件（操作步驟、checklist）的祈使是合理 stance、不適用本卡。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於 review <a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式</a> 時、由讀者連續指出三種對讀者喊話：</p>
<ol>
<li><strong>安撫</strong>：「很多人卡在『哪裡是型別、哪裡是名字』」—— 訴諸群體、讓讀者覺得不孤單。</li>
<li><strong>第二人稱</strong>：「順序跟你天天寫的 <code>int count</code> 一樣」「你天天在用 map」—— 把讀者拉進句子。</li>
<li><strong>祈使</strong>：小節標題「先讀懂這個簽章的每個部分」「先釐清：什麼是 X」—— 指揮閱讀順序。</li>
</ol>
<p>三者在 grep keyword bank 的表現不一（安撫有部分關鍵詞「很多人」、第二人稱跟祈使幾乎無固定詞）、但共享同一個違反 —— <strong>教材在對讀者喊話、而非中性陳述概念</strong>。修法統一：安撫句刪、第二人稱改中性指稱、祈使標題改描述性名詞標題。</p>
<p>對應本卡：對讀者喊話是獨立於精度（#111）跟 review-process（#149）的 content register 議題 —— 一個句子可以精度完全正確、grep 完全乾淨、卻因為 stance 錯（在跟讀者說話）而不該留在教材裡。</p>
]]></content:encoded></item><item><title>教材給技術理由、不替方案下品質評價</title><link>https://tarrragon.github.io/blog/report/teaching-gives-reasons-not-quality-verdicts/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/teaching-gives-reasons-not-quality-verdicts/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的建卡判準是&lt;strong>教學需求、不是出現頻率&lt;/strong>（對應 AGENTS 知識卡片規範段：建卡看「讀者缺這個知識會不會難以理解教材」、出現頻率只是補充訊號）。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>觸發 case 只有 1 個實例&lt;/strong>：這次 review 的 HOF 文章只出現一處「教科書級」。建卡的正當性來自「自評誇飾在技術寫作裡是常見 pattern」這個教學需求、不是這篇的頻率。單篇頻率低不影響值得建卡。&lt;/li>
&lt;li>&lt;strong>誇飾詞是 starting set、不是窮舉&lt;/strong>：列出的（教科書級 / 堪稱 / 完美 / 漂亮地）是觀察到的、不代表完整詞庫。新誇飾（神級 / 教科書式 / 無懈可擊）出現時持續擴充。&lt;/li>
&lt;li>&lt;strong>跟 #111 誇張家族部分重疊&lt;/strong>：本卡跟 #111 同屬「誇飾」大類、靠「評價對象」區分（見關係段）。邊界可能在某些句子模糊、需個案判斷。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>教材的責任是給出「為什麼這樣設計 / 為什麼適配」的技術理由，不對方案下品質評價。&lt;/strong> 自評誇飾（「X 是教科書級的適配」「堪稱經典」「完美契合」）讀起來像作者在替自己的方案打分數 —— 它傳遞的是作者的滿意度、不是讀者能用的概念。&lt;/p>
&lt;p>更深的問題是&lt;strong>品質評價會頂替技術理由&lt;/strong>：寫了「X 是教科書級的適配」、就佔掉了本該寫「X 為什麼適配」的位置。讀者拿到一個 verdict（這方案很好）、但拿不到 reasoning（好在哪、適配條件是什麼、邊界在哪）。誇飾是「&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>「X 是教科書級的適配」&lt;/td>
 &lt;td>作者覺得很適配&lt;/td>
 &lt;td>X 對上的具體結構（固定流程寫死、變化點抽參數）&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>契合的機制（更新本質是 current → next、函式參數正好表達）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「漂亮地解決了 X」&lt;/td>
 &lt;td>作者覺得很漂亮&lt;/td>
 &lt;td>解法的步驟 + 為什麼這樣最省&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="具體-case">具體 case&lt;/h2>
&lt;p>&lt;strong>誇飾版&lt;/strong>：「當『共用流程』與『變化點』能這樣切乾淨時，HOF 是&lt;strong>教科書級的適配&lt;/strong>：把固定流程寫死在 &lt;code>update&lt;/code> 裡，把變化點抽成函式參數 &lt;code>transform&lt;/code> 由呼叫端帶入。」&lt;/p>
&lt;p>&lt;strong>問題&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>「教科書級」是品質評價、不是技術內容 —— 讀者無法反推「適配到什麼程度、什麼條件下成立、邊界在哪」。&lt;/li>
&lt;li>它頂替了理由的位置：冒號後面的「把固定流程寫死、把變化點抽成參數」其實才是真正的適配理由、但被「教科書級」這個 verdict 搶了焦點。&lt;/li>
&lt;li>沒有教學會說某寫法是「教科書寫法」—— 這句話的語氣是個人檢討在誇方案、不是教材在講概念。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>修法版&lt;/strong>：「當『共用流程』與『變化點』能這樣切乾淨時，HOF &lt;strong>正好對上這個結構&lt;/strong>：把固定流程寫死在 &lt;code>update&lt;/code> 裡，把變化點抽成函式參數 &lt;code>transform&lt;/code> 由呼叫端帶入。」&lt;/p>
&lt;p>差別：「正好對上這個結構」是中性的適配陳述、把焦點交給冒號後面的具體機制；「教科書級」是 verdict、把焦點留在作者的評價上。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="讀者拿到-verdict拿不到可遷移的判準">讀者拿到 verdict、拿不到可遷移的判準&lt;/h3>
&lt;p>「教科書級的適配」讀完、讀者只知道「作者覺得這很適配」、無法判斷自己的 case 適不適配。教學的目的是讓讀者拿到能套用到新情境的 reasoning —— 品質評價給不了這個。這違反 AGENTS 原則七（每段要有可操作判準、不能只做評價）。&lt;/p>
&lt;h3 id="評價會掩蓋理由其實沒寫">評價會掩蓋「理由其實沒寫」&lt;/h3>
&lt;p>誇飾讀起來很有說服力、容易讓作者跟 reviewer 都以為「這段講清楚了」。實際上 verdict 佔了版面、理由可能根本沒展開 —— 跟 #94 的空降斷言同構：包裝成肯定句、但依據是空的。&lt;/p>
&lt;h3 id="語氣從教材滑向個人檢討">語氣從教材滑向個人檢討&lt;/h3>
&lt;p>「教科書級 / 堪稱經典」是回顧視角的自我評價（我這方案選得好）。教材是面向讀者講概念、不是面向自己總結成果 —— 語氣一滑、整段的 register 就從「教學」變成「炫耀 / 自評」。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度&lt;/a>&lt;/strong>：兩卡同屬「誇飾」大類、靠&lt;strong>評價對象&lt;/strong>區分。#111 的誇飾誇張的是&lt;strong>技術屬性&lt;/strong>（「一輩子」誇張時間、「整個炸了」誇張嚴重度）—— 是對技術事實的不精確描述。本卡的誇飾評價的是&lt;strong>方案品質&lt;/strong>（「教科書級」評價設計好壞）—— 根本不在描述技術事實、是 verdict 頂替 reasoning。#111 修法是「翻譯回技術屬性」、本卡修法是「換成技術理由」。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../teaching-register-states-not-addresses-reader/">#150 教材用中性陳述、不對讀者喊話&lt;/a>&lt;/strong>：兩卡是 register/stance 議題的 sibling。#150 的違反是「&lt;strong>管理讀者&lt;/strong>」（安撫 / 代入 / 指揮）、本卡的違反是「&lt;strong>評價方案&lt;/strong>」（替自己的設計打分數）。兩者都背離「教材中性陳述概念」這個上層 stance、但對象不同（讀者 vs 方案）。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據、不能空降結論&lt;/a>&lt;/strong>：本卡是 #94「空降斷言」在「品質評價」維度的變體。#94 處理「刪掉對照 Y → 結論 X 變空降」、本卡處理「誇飾 verdict 頂替理由 → 評價變空降」。兩者共享同一個失敗：給結論不給依據、讀者只能選擇相信或腦補。&lt;/li>
&lt;li>&lt;strong>AGENTS 原則七（每段要有可操作判準）&lt;/strong>：誇飾直接違反原則七 —— verdict 不是判準。本卡是原則七在「自評誇飾」這個具體違反形式上的展開。&lt;/li>
&lt;/ul>
&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>出現「教科書級 / 堪稱 / 完美 / 經典 / 漂亮地 / 優雅地」&lt;/td>
 &lt;td>自評誇飾 —— 換成具體技術理由 / 機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>句子在說「這方案有多好」而非「這方案怎麼運作 / 為什麼成立」&lt;/td>
 &lt;td>verdict 頂替 reasoning —— 補回理由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪掉誇飾詞後、該段的適配理由仍完整&lt;/td>
 &lt;td>誇飾是純贅字 —— 直接刪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪掉誇飾詞後、該段只剩 verdict、沒有理由&lt;/td>
 &lt;td>理由根本沒寫 —— 補機制 / 條件、不是補誇飾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語氣像在總結「我選得好」而非對讀者講概念&lt;/td>
 &lt;td>register 滑向個人檢討 —— 改回中性陳述&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：概念建立段、設計理由段、適配性論述 —— 這些地方該給機制 / 條件、不該給品質評價。&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：純 narrative / 復盤型內容（work-log 的事件回顧、retrospective）—— 這類內容本就含作者視角的評價、誇飾是合理 register。&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：中性化 ≠ 禁止一切「適配 / 適合」字眼。判別線是「這句給的是&lt;strong>可遷移的判讀條件&lt;/strong>（三條件齊備時 HOF 比列舉更省）還是&lt;strong>品質 verdict&lt;/strong>（教科書級）」—— 前者是判準該留、後者是評價該換。帶條件的適配陳述不是誇飾。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源&lt;/h2>
&lt;p>本卡觸發於 review &lt;a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式&lt;/a> 時、讀者指出「HOF 是教科書級的適配」這句 —— 「這比較像個人檢討、沒有教學會說這是教科書寫法」。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的建卡判準是<strong>教學需求、不是出現頻率</strong>（對應 AGENTS 知識卡片規範段：建卡看「讀者缺這個知識會不會難以理解教材」、出現頻率只是補充訊號）。具體限制：</p>
<ul>
<li><strong>觸發 case 只有 1 個實例</strong>：這次 review 的 HOF 文章只出現一處「教科書級」。建卡的正當性來自「自評誇飾在技術寫作裡是常見 pattern」這個教學需求、不是這篇的頻率。單篇頻率低不影響值得建卡。</li>
<li><strong>誇飾詞是 starting set、不是窮舉</strong>：列出的（教科書級 / 堪稱 / 完美 / 漂亮地）是觀察到的、不代表完整詞庫。新誇飾（神級 / 教科書式 / 無懈可擊）出現時持續擴充。</li>
<li><strong>跟 #111 誇張家族部分重疊</strong>：本卡跟 #111 同屬「誇飾」大類、靠「評價對象」區分（見關係段）。邊界可能在某些句子模糊、需個案判斷。</li>
</ul>
<hr>
<h2 id="核心原則">核心原則</h2>
<p><strong>教材的責任是給出「為什麼這樣設計 / 為什麼適配」的技術理由，不對方案下品質評價。</strong> 自評誇飾（「X 是教科書級的適配」「堪稱經典」「完美契合」）讀起來像作者在替自己的方案打分數 —— 它傳遞的是作者的滿意度、不是讀者能用的概念。</p>
<p>更深的問題是<strong>品質評價會頂替技術理由</strong>：寫了「X 是教科書級的適配」、就佔掉了本該寫「X 為什麼適配」的位置。讀者拿到一個 verdict（這方案很好）、但拿不到 reasoning（好在哪、適配條件是什麼、邊界在哪）。誇飾是「<strong>評價空降</strong>」—— 給結論不給依據。</p>
<table>
  <thead>
      <tr>
          <th>自評誇飾</th>
          <th>讀者拿到的</th>
          <th>該換成的技術理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「X 是教科書級的適配」</td>
          <td>作者覺得很適配</td>
          <td>X 對上的具體結構（固定流程寫死、變化點抽參數）</td>
      </tr>
      <tr>
          <td>「堪稱經典的設計」</td>
          <td>作者覺得很經典</td>
          <td>這設計解決的具體問題 + 成立條件</td>
      </tr>
      <tr>
          <td>「完美契合不可變模型」</td>
          <td>作者覺得很契合</td>
          <td>契合的機制（更新本質是 current → next、函式參數正好表達）</td>
      </tr>
      <tr>
          <td>「漂亮地解決了 X」</td>
          <td>作者覺得很漂亮</td>
          <td>解法的步驟 + 為什麼這樣最省</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="具體-case">具體 case</h2>
<p><strong>誇飾版</strong>：「當『共用流程』與『變化點』能這樣切乾淨時，HOF 是<strong>教科書級的適配</strong>：把固定流程寫死在 <code>update</code> 裡，把變化點抽成函式參數 <code>transform</code> 由呼叫端帶入。」</p>
<p><strong>問題</strong>：</p>
<ul>
<li>「教科書級」是品質評價、不是技術內容 —— 讀者無法反推「適配到什麼程度、什麼條件下成立、邊界在哪」。</li>
<li>它頂替了理由的位置：冒號後面的「把固定流程寫死、把變化點抽成參數」其實才是真正的適配理由、但被「教科書級」這個 verdict 搶了焦點。</li>
<li>沒有教學會說某寫法是「教科書寫法」—— 這句話的語氣是個人檢討在誇方案、不是教材在講概念。</li>
</ul>
<p><strong>修法版</strong>：「當『共用流程』與『變化點』能這樣切乾淨時，HOF <strong>正好對上這個結構</strong>：把固定流程寫死在 <code>update</code> 裡，把變化點抽成函式參數 <code>transform</code> 由呼叫端帶入。」</p>
<p>差別：「正好對上這個結構」是中性的適配陳述、把焦點交給冒號後面的具體機制；「教科書級」是 verdict、把焦點留在作者的評價上。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="讀者拿到-verdict拿不到可遷移的判準">讀者拿到 verdict、拿不到可遷移的判準</h3>
<p>「教科書級的適配」讀完、讀者只知道「作者覺得這很適配」、無法判斷自己的 case 適不適配。教學的目的是讓讀者拿到能套用到新情境的 reasoning —— 品質評價給不了這個。這違反 AGENTS 原則七（每段要有可操作判準、不能只做評價）。</p>
<h3 id="評價會掩蓋理由其實沒寫">評價會掩蓋「理由其實沒寫」</h3>
<p>誇飾讀起來很有說服力、容易讓作者跟 reviewer 都以為「這段講清楚了」。實際上 verdict 佔了版面、理由可能根本沒展開 —— 跟 #94 的空降斷言同構：包裝成肯定句、但依據是空的。</p>
<h3 id="語氣從教材滑向個人檢討">語氣從教材滑向個人檢討</h3>
<p>「教科書級 / 堪稱經典」是回顧視角的自我評價（我這方案選得好）。教材是面向讀者講概念、不是面向自己總結成果 —— 語氣一滑、整段的 register 就從「教學」變成「炫耀 / 自評」。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度</a></strong>：兩卡同屬「誇飾」大類、靠<strong>評價對象</strong>區分。#111 的誇飾誇張的是<strong>技術屬性</strong>（「一輩子」誇張時間、「整個炸了」誇張嚴重度）—— 是對技術事實的不精確描述。本卡的誇飾評價的是<strong>方案品質</strong>（「教科書級」評價設計好壞）—— 根本不在描述技術事實、是 verdict 頂替 reasoning。#111 修法是「翻譯回技術屬性」、本卡修法是「換成技術理由」。</li>
<li><strong><a href="../teaching-register-states-not-addresses-reader/">#150 教材用中性陳述、不對讀者喊話</a></strong>：兩卡是 register/stance 議題的 sibling。#150 的違反是「<strong>管理讀者</strong>」（安撫 / 代入 / 指揮）、本卡的違反是「<strong>評價方案</strong>」（替自己的設計打分數）。兩者都背離「教材中性陳述概念」這個上層 stance、但對象不同（讀者 vs 方案）。</li>
<li><strong><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據、不能空降結論</a></strong>：本卡是 #94「空降斷言」在「品質評價」維度的變體。#94 處理「刪掉對照 Y → 結論 X 變空降」、本卡處理「誇飾 verdict 頂替理由 → 評價變空降」。兩者共享同一個失敗：給結論不給依據、讀者只能選擇相信或腦補。</li>
<li><strong>AGENTS 原則七（每段要有可操作判準）</strong>：誇飾直接違反原則七 —— verdict 不是判準。本卡是原則七在「自評誇飾」這個具體違反形式上的展開。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>出現「教科書級 / 堪稱 / 完美 / 經典 / 漂亮地 / 優雅地」</td>
          <td>自評誇飾 —— 換成具體技術理由 / 機制</td>
      </tr>
      <tr>
          <td>句子在說「這方案有多好」而非「這方案怎麼運作 / 為什麼成立」</td>
          <td>verdict 頂替 reasoning —— 補回理由</td>
      </tr>
      <tr>
          <td>刪掉誇飾詞後、該段的適配理由仍完整</td>
          <td>誇飾是純贅字 —— 直接刪</td>
      </tr>
      <tr>
          <td>刪掉誇飾詞後、該段只剩 verdict、沒有理由</td>
          <td>理由根本沒寫 —— 補機制 / 條件、不是補誇飾</td>
      </tr>
      <tr>
          <td>語氣像在總結「我選得好」而非對讀者講概念</td>
          <td>register 滑向個人檢討 —— 改回中性陳述</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：概念建立段、設計理由段、適配性論述 —— 這些地方該給機制 / 條件、不該給品質評價。</li>
<li><strong>不適用</strong>：純 narrative / 復盤型內容（work-log 的事件回顧、retrospective）—— 這類內容本就含作者視角的評價、誇飾是合理 register。</li>
<li><strong>邊界</strong>：中性化 ≠ 禁止一切「適配 / 適合」字眼。判別線是「這句給的是<strong>可遷移的判讀條件</strong>（三條件齊備時 HOF 比列舉更省）還是<strong>品質 verdict</strong>（教科書級）」—— 前者是判準該留、後者是評價該換。帶條件的適配陳述不是誇飾。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於 review <a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式</a> 時、讀者指出「HOF 是教科書級的適配」這句 —— 「這比較像個人檢討、沒有教學會說這是教科書寫法」。</p>
<p>關鍵判讀：這句<strong>精度沒問題</strong>（HOF 確實適配這個結構）、<strong>grep 也不一定命中</strong>（「教科書級」不在口語修辭 keyword bank）、但 register 錯了 —— 它在評價方案品質、不在陳述技術理由。修法是把「教科書級的適配」換成「正好對上這個結構」、讓焦點回到冒號後面的具體機制。</p>
<p>建卡的正當性來自<strong>教學需求</strong>、不是這篇的頻率：自評誇飾在技術寫作裡是常見 pattern、讀者 / 作者都容易把「verdict 讀起來有說服力」誤當「理由講清楚了」。這篇只有 1 個實例、但 pattern 普遍 —— 對應 AGENTS 知識卡片規範段「建卡看教學需求、出現頻率只是補充訊號」。</p>
]]></content:encoded></item><item><title>教材把設計選擇講成選擇、不講成必然或天性</title><link>https://tarrragon.github.io/blog/report/design-choices-framed-as-choices-not-necessity/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/design-choices-framed-as-choices-not-necessity/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>1 個 case&lt;/strong>（&lt;a href="../../work-log/dart_hof_typedef_readability/">HOF / typedef 文章&lt;/a>的「更新的本質天生就是一個函式」）抽出來的觀察、建卡判準是教學需求（本質主義框架在技術寫作、尤其 LLM 生成內容裡是高發 pattern）。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>必然性詞是 starting set&lt;/strong>：天生 / 本質就是 / 必然 / 唯一 / 註定 是觀察到的、不窮舉。新形式（生來 / 理所當然 / 不可能不）持續擴充。&lt;/li>
&lt;li>&lt;strong>跟 compositional-writing 原則三（機會成本語氣）部分重疊&lt;/strong>：本卡是原則三的一個 subtype、靠「絕對的形式」區分（命令式 vs 必然式、見關係段）。原則三未獨立成 report 卡、本卡補這個缺口的必然性維度。&lt;/li>
&lt;li>&lt;strong>「設計 vs 必然」的界線本身是判斷&lt;/strong>：某些陳述介於「被上游選擇逼出來的後果」跟「真正的自然必然」之間（見邊界段）、需個案判讀。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>設計選擇要講成選擇 —— 說明它在什麼條件下成立、為什麼比替代方案好，而不是用「天生 / 本質就是 / 必然」把它講成自然法則。&lt;/strong> 必然性框架抹掉設計能動性：讀者拿到「這本來就這樣」、卻不知道「這是一個選擇、有前提、有替代」。&lt;/p>
&lt;p>這是「機會成本語氣 vs 絕對主義」違反的一個 subtype，但比命令式絕對更隱形：&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>「應該用 X」「正確做法是 X」&lt;/td>
 &lt;td>讀者聽得出這是主張、會質疑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>必然式（本卡）&lt;/strong>&lt;/td>
 &lt;td>「X 天生就是 Y」「本質就是 Y」「必然是 Y」&lt;/td>
 &lt;td>偽裝成事實陳述、讀者不會質疑「天性」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>必然式更該防 —— 它不說「你該怎麼做」（讀者會審），而說「事情本來就這樣」（讀者直接接受）、把一個 trade-off 藏進「自然」的外衣。&lt;/p>
&lt;hr>
&lt;h2 id="具體-case">具體 case&lt;/h2>
&lt;p>&lt;strong>必然版&lt;/strong>：「更新的本質&lt;strong>天生就是&lt;/strong>一個 &lt;code>(current) =&amp;gt; next&lt;/code> 的函式 — 拿舊值算出新值。」&lt;/p>
&lt;p>&lt;strong>問題&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>update 是設計出來的方法、不是與生俱來的東西、「天生」語義場（生物性 / 本能）對不上設計產物（表層症狀）。&lt;/li>
&lt;li>更深：update 以 &lt;code>(current) =&amp;gt; next&lt;/code> 形式呈現、&lt;strong>只在「選了不可變模型」之後才成立&lt;/strong>。不可變本身是設計選擇（&lt;code>@immutable&lt;/code> + &lt;code>copyWith&lt;/code>）—— 「天生」把這個前提條件跳過、讓條件性結論變成無條件真理。&lt;/li>
&lt;li>最深：&lt;strong>牴觸文章自己的論點&lt;/strong>。這篇通篇論證 HOF 是&lt;strong>條件性&lt;/strong>適配（「三條件齊備才划算」「場景不對時硬用是過度設計」）—— 唯獨這句用「天生」把它講成必然、局部破壞了文章自己的條件性紀律。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>修法版&lt;/strong>：「&lt;strong>不可變模型下的&lt;/strong>更新，在語意上&lt;strong>就是&lt;/strong>一個 &lt;code>(current) =&amp;gt; next&lt;/code> 的函式 — 拿舊值算出新值。」&lt;/p>
&lt;p>差別：修法版用「不可變模型下的」把前提條件講出來、用「就是」取代「天生」—— 結論還在、但還原成「在某前提下成立」、不再偽裝成自然法則。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="讀者學到規則而非思考方式">讀者學到「規則」而非「思考方式」&lt;/h3>
&lt;p>必然框架告訴讀者「事情本來就這樣」、讀者拿到一條沒有條件的規則。換個情境（可變模型、需要 persist 的場景）規則就不適用、但讀者不知道邊界在哪 —— 因為「天生」沒給條件。機會成本語氣的核心就是「教思考方式（能遷移）而非規則（壓力下會忘）」、必然框架直接違反。&lt;/p>
&lt;h3 id="局部牴觸自身論點瓦解可信度">局部牴觸自身論點、瓦解可信度&lt;/h3>
&lt;p>一篇講「看條件、不是必然」的文章、出現一句「天生 / 必然」、細心的讀者會發現矛盾 —— 到底是條件性還是必然？這比單純用詞不當更傷：它瓦解文章整體論證的一致性。本質主義框架的高發處正是「作者其實知道是條件性、但順手用了必然語氣」。&lt;/p>
&lt;h3 id="偽裝成事實躲過-review">偽裝成事實、躲過 review&lt;/h3>
&lt;p>「應該用 X」這種命令式主張、reviewer 會審「憑什麼應該」。「X 天生就是 Y」偽裝成事實陳述、reviewer 容易直接接受 —— 跟 #151 誇飾、#94 空降斷言同構：把需要依據的東西包裝成不需要依據的形式、躲過檢查。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>compositional-writing 原則三（機會成本語氣優先）&lt;/strong>：本卡是原則三的 subtype。原則三主要談命令式絕對（「應該 / 正確做法是 / 替代方案不足」）、本卡補&lt;strong>必然式絕對&lt;/strong>（天生 / 本質就是 / 必然）這個更隱形的維度。原則三未獨立成 report 卡 —— 本卡是它在「必然性框架」這個具體形式上的 report 化。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../teaching-gives-reasons-not-quality-verdicts/">#151 教材給技術理由、不替方案下品質評價&lt;/a>&lt;/strong>：兩卡是「空降家族」的 sibling、都把需要依據的東西包裝成不需要依據。#151 是&lt;strong>品質 verdict 空降&lt;/strong>（教科書級頂替理由）、本卡是&lt;strong>必然框架空降&lt;/strong>（天生頂替條件）。差別在頂替的東西：#151 頂替「為什麼好」、本卡頂替「在什麼條件下成立」。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據、不能空降結論&lt;/a>&lt;/strong>：#94 是空降家族的源頭（刪掉對照 Y → 結論 X 空降）。本卡跟 #151 都是 #94 在不同維度的變體 —— 三者共享「給結論不給依據、讀者只能相信或腦補」的失敗。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度&lt;/a>&lt;/strong>：「天生」在表層也是一種用詞精度問題（語義場對不上指涉對象）—— 但本卡聚焦的是它的&lt;strong>框架效果&lt;/strong>（把設計絕對化）、不是精度。#111 抓表層用詞、本卡抓深層框架、可疊加。&lt;/li>
&lt;/ul>
&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>出現「天生 / 與生俱來 / 本質就是 / 必然 / 唯一 / 註定 / 理所當然」&lt;/td>
 &lt;td>檢查指涉對象是不是設計產物 —— 是 → 還原成條件性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>句子說「X 本來就是 Y」、但 X 是設計出來的&lt;/td>
 &lt;td>必然框架 —— 補前提條件（「在選了 Z 之後、X 以 Y 形式成立」）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一篇別處強調「看條件 / 看場景 / trade-off」&lt;/td>
 &lt;td>檢查必然句是否牴觸自身條件性論點 —— 是 → 對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>必然陳述讀起來像事實、但其實是設計決策&lt;/td>
 &lt;td>偽裝成事實的主張 —— 還原成「選擇 + 理由 + 條件」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪掉「天生 / 必然」後句子仍成立&lt;/td>
 &lt;td>詞是純贅字 / 偽裝 —— 直接刪或還原條件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：設計理由段、概念建立段、適配性論述 —— 凡是在講「某設計 / 某結構為什麼成立」的地方。&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：物理 / 法律 / 合規 / 數學事實（per 原則三的例外）—— 「雜湊碰撞必然存在」「GDPR 規定必須 X」這類是真必然、可用必然語氣。&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：條件性後果 vs 真必然的判別線是「&lt;strong>這個必然有沒有上游的設計選擇當前提&lt;/strong>」。「不可變模型下、更新是 current → next」是條件性（前提=選了不可變）、要講出前提；「任何雜湊函式都有碰撞」是真必然（不依賴任何設計選擇）、可直接斷言。被上游選擇逼出來的後果、不是天性。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源&lt;/h2>
&lt;p>本卡觸發於 review &lt;a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式&lt;/a> 時、讀者指出「更新的本質&lt;strong>天生&lt;/strong>就是一個函式」—— 「不會有天生這件事、update 也是設計出來的、中文語義上不可能說這叫天生」。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>1 個 case</strong>（<a href="../../work-log/dart_hof_typedef_readability/">HOF / typedef 文章</a>的「更新的本質天生就是一個函式」）抽出來的觀察、建卡判準是教學需求（本質主義框架在技術寫作、尤其 LLM 生成內容裡是高發 pattern）。具體限制：</p>
<ul>
<li><strong>必然性詞是 starting set</strong>：天生 / 本質就是 / 必然 / 唯一 / 註定 是觀察到的、不窮舉。新形式（生來 / 理所當然 / 不可能不）持續擴充。</li>
<li><strong>跟 compositional-writing 原則三（機會成本語氣）部分重疊</strong>：本卡是原則三的一個 subtype、靠「絕對的形式」區分（命令式 vs 必然式、見關係段）。原則三未獨立成 report 卡、本卡補這個缺口的必然性維度。</li>
<li><strong>「設計 vs 必然」的界線本身是判斷</strong>：某些陳述介於「被上游選擇逼出來的後果」跟「真正的自然必然」之間（見邊界段）、需個案判讀。</li>
</ul>
<hr>
<h2 id="核心原則">核心原則</h2>
<p><strong>設計選擇要講成選擇 —— 說明它在什麼條件下成立、為什麼比替代方案好，而不是用「天生 / 本質就是 / 必然」把它講成自然法則。</strong> 必然性框架抹掉設計能動性：讀者拿到「這本來就這樣」、卻不知道「這是一個選擇、有前提、有替代」。</p>
<p>這是「機會成本語氣 vs 絕對主義」違反的一個 subtype，但比命令式絕對更隱形：</p>
<table>
  <thead>
      <tr>
          <th>絕對主義形式</th>
          <th>句型</th>
          <th>為什麼隱形</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>命令式（原則三主要談的）</td>
          <td>「應該用 X」「正確做法是 X」</td>
          <td>讀者聽得出這是主張、會質疑</td>
      </tr>
      <tr>
          <td><strong>必然式（本卡）</strong></td>
          <td>「X 天生就是 Y」「本質就是 Y」「必然是 Y」</td>
          <td>偽裝成事實陳述、讀者不會質疑「天性」</td>
      </tr>
  </tbody>
</table>
<p>必然式更該防 —— 它不說「你該怎麼做」（讀者會審），而說「事情本來就這樣」（讀者直接接受）、把一個 trade-off 藏進「自然」的外衣。</p>
<hr>
<h2 id="具體-case">具體 case</h2>
<p><strong>必然版</strong>：「更新的本質<strong>天生就是</strong>一個 <code>(current) =&gt; next</code> 的函式 — 拿舊值算出新值。」</p>
<p><strong>問題</strong>：</p>
<ul>
<li>update 是設計出來的方法、不是與生俱來的東西、「天生」語義場（生物性 / 本能）對不上設計產物（表層症狀）。</li>
<li>更深：update 以 <code>(current) =&gt; next</code> 形式呈現、<strong>只在「選了不可變模型」之後才成立</strong>。不可變本身是設計選擇（<code>@immutable</code> + <code>copyWith</code>）—— 「天生」把這個前提條件跳過、讓條件性結論變成無條件真理。</li>
<li>最深：<strong>牴觸文章自己的論點</strong>。這篇通篇論證 HOF 是<strong>條件性</strong>適配（「三條件齊備才划算」「場景不對時硬用是過度設計」）—— 唯獨這句用「天生」把它講成必然、局部破壞了文章自己的條件性紀律。</li>
</ul>
<p><strong>修法版</strong>：「<strong>不可變模型下的</strong>更新，在語意上<strong>就是</strong>一個 <code>(current) =&gt; next</code> 的函式 — 拿舊值算出新值。」</p>
<p>差別：修法版用「不可變模型下的」把前提條件講出來、用「就是」取代「天生」—— 結論還在、但還原成「在某前提下成立」、不再偽裝成自然法則。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="讀者學到規則而非思考方式">讀者學到「規則」而非「思考方式」</h3>
<p>必然框架告訴讀者「事情本來就這樣」、讀者拿到一條沒有條件的規則。換個情境（可變模型、需要 persist 的場景）規則就不適用、但讀者不知道邊界在哪 —— 因為「天生」沒給條件。機會成本語氣的核心就是「教思考方式（能遷移）而非規則（壓力下會忘）」、必然框架直接違反。</p>
<h3 id="局部牴觸自身論點瓦解可信度">局部牴觸自身論點、瓦解可信度</h3>
<p>一篇講「看條件、不是必然」的文章、出現一句「天生 / 必然」、細心的讀者會發現矛盾 —— 到底是條件性還是必然？這比單純用詞不當更傷：它瓦解文章整體論證的一致性。本質主義框架的高發處正是「作者其實知道是條件性、但順手用了必然語氣」。</p>
<h3 id="偽裝成事實躲過-review">偽裝成事實、躲過 review</h3>
<p>「應該用 X」這種命令式主張、reviewer 會審「憑什麼應該」。「X 天生就是 Y」偽裝成事實陳述、reviewer 容易直接接受 —— 跟 #151 誇飾、#94 空降斷言同構：把需要依據的東西包裝成不需要依據的形式、躲過檢查。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong>compositional-writing 原則三（機會成本語氣優先）</strong>：本卡是原則三的 subtype。原則三主要談命令式絕對（「應該 / 正確做法是 / 替代方案不足」）、本卡補<strong>必然式絕對</strong>（天生 / 本質就是 / 必然）這個更隱形的維度。原則三未獨立成 report 卡 —— 本卡是它在「必然性框架」這個具體形式上的 report 化。</li>
<li><strong><a href="../teaching-gives-reasons-not-quality-verdicts/">#151 教材給技術理由、不替方案下品質評價</a></strong>：兩卡是「空降家族」的 sibling、都把需要依據的東西包裝成不需要依據。#151 是<strong>品質 verdict 空降</strong>（教科書級頂替理由）、本卡是<strong>必然框架空降</strong>（天生頂替條件）。差別在頂替的東西：#151 頂替「為什麼好」、本卡頂替「在什麼條件下成立」。</li>
<li><strong><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據、不能空降結論</a></strong>：#94 是空降家族的源頭（刪掉對照 Y → 結論 X 空降）。本卡跟 #151 都是 #94 在不同維度的變體 —— 三者共享「給結論不給依據、讀者只能相信或腦補」的失敗。</li>
<li><strong><a href="../colloquial-rhetoric-erodes-technical-precision/">#111 口語化修辭會稀釋技術精度</a></strong>：「天生」在表層也是一種用詞精度問題（語義場對不上指涉對象）—— 但本卡聚焦的是它的<strong>框架效果</strong>（把設計絕對化）、不是精度。#111 抓表層用詞、本卡抓深層框架、可疊加。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>出現「天生 / 與生俱來 / 本質就是 / 必然 / 唯一 / 註定 / 理所當然」</td>
          <td>檢查指涉對象是不是設計產物 —— 是 → 還原成條件性</td>
      </tr>
      <tr>
          <td>句子說「X 本來就是 Y」、但 X 是設計出來的</td>
          <td>必然框架 —— 補前提條件（「在選了 Z 之後、X 以 Y 形式成立」）</td>
      </tr>
      <tr>
          <td>同一篇別處強調「看條件 / 看場景 / trade-off」</td>
          <td>檢查必然句是否牴觸自身條件性論點 —— 是 → 對齊</td>
      </tr>
      <tr>
          <td>必然陳述讀起來像事實、但其實是設計決策</td>
          <td>偽裝成事實的主張 —— 還原成「選擇 + 理由 + 條件」</td>
      </tr>
      <tr>
          <td>刪掉「天生 / 必然」後句子仍成立</td>
          <td>詞是純贅字 / 偽裝 —— 直接刪或還原條件</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：設計理由段、概念建立段、適配性論述 —— 凡是在講「某設計 / 某結構為什麼成立」的地方。</li>
<li><strong>不適用</strong>：物理 / 法律 / 合規 / 數學事實（per 原則三的例外）—— 「雜湊碰撞必然存在」「GDPR 規定必須 X」這類是真必然、可用必然語氣。</li>
<li><strong>邊界</strong>：條件性後果 vs 真必然的判別線是「<strong>這個必然有沒有上游的設計選擇當前提</strong>」。「不可變模型下、更新是 current → next」是條件性（前提=選了不可變）、要講出前提；「任何雜湊函式都有碰撞」是真必然（不依賴任何設計選擇）、可直接斷言。被上游選擇逼出來的後果、不是天性。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於 review <a href="../../work-log/dart_hof_typedef_readability/">為什麼這個場景適合用高階函式</a> 時、讀者指出「更新的本質<strong>天生</strong>就是一個函式」—— 「不會有天生這件事、update 也是設計出來的、中文語義上不可能說這叫天生」。</p>
<p>第一輪我把它定性成單純的語義錯置（天生=生物性、對不上設計產物）、讀者要求用 WRAP 再想一次。WRAP 揭露三層遞進：(1) 表層語義場錯置；(2) 中層把設計選擇講成必然、抹掉設計能動性；(3) 深層<strong>牴觸文章自己的條件性論點</strong>（通篇論證 HOF 是條件性適配、唯獨這句講成天生）。</p>
<p>對應本卡：必然性框架不是用詞問題、是<strong>框架問題</strong> —— 把「設計」講成「天性」。它比命令式絕對（#151 / 原則三主談）更隱形、因為偽裝成事實陳述躲過讀者跟 reviewer 的審查；它的高發訊號是「局部牴觸作者自己在別處的條件性立場」。建卡正當性來自教學需求（LLM 寫作高發）、非本 case 頻率（1 實例）。</p>
]]></content:encoded></item><item><title>Review 漏抓先分 design gap 與 execution gap、再決定改框架還是改執行</title><link>https://tarrragon.github.io/blog/report/review-miss-diagnose-design-vs-execution-gap/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/review-miss-diagnose-design-vs-execution-gap/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡是一次 review 失誤的 self-retrospective（用 WRAP 的 Consider the Opposite 反向檢驗自己的 review 過程）抽出。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>樣本是 1 次 review、1 個 reviewer（我自己）&lt;/strong>：「design gap vs execution gap」這個二分基於單次自我檢討、不是跨多次 review 觀察到的 systematic pattern。&lt;/li>
&lt;li>&lt;strong>「自我歸因」有 self-serving 風險&lt;/strong>：reviewer 檢討自己漏抓時、可能傾向把責任推給「框架不足」（design gap）而非「我偷懶」（execution gap）—— 本卡的價值正是強迫拆開這兩者、但拆的人就是當事人、客觀性有限。&lt;/li>
&lt;li>&lt;strong>修法有效性未驗證&lt;/strong>：診斷後的修法（改框架 vs 改執行）是否真能讓下次 catch 到、未經後續 review 驗證。&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、把它當「review 失誤歸因的一個檢查步驟」、不當「驗證過的 review 流程」。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>Review 漏抓某類問題時，先分清是 &lt;strong>design gap&lt;/strong> 還是 &lt;strong>execution gap&lt;/strong>，再決定修法 —— 兩者修法相反。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Design gap&lt;/strong>：框架根本沒有對應的 frame / keyword 去 catch 這類問題。修法是&lt;strong>改框架&lt;/strong>（補 frame、補 keyword bank、補 lens）。&lt;/li>
&lt;li>&lt;strong>Execution gap&lt;/strong>：框架有對應 frame、但 reviewer 這次沒跑（跑了子集、跳過該跑的輪）。修法是&lt;strong>改執行&lt;/strong>（真的跑完該跑的輪），改框架沒用。&lt;/li>
&lt;/ul>
&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>Design gap&lt;/td>
 &lt;td>框架缺 frame&lt;/td>
 &lt;td>補 frame / keyword / lens&lt;/td>
 &lt;td>誤判成 execution → 一直漏同類（以為「下次認真跑」就好、但根本沒對應 frame）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Execution gap&lt;/td>
 &lt;td>沒跑該跑的輪&lt;/td>
 &lt;td>跑完整框架&lt;/td>
 &lt;td>誤判成 design → framework bloat（一直加 frame、卻沒解決「偷跑子集」的習慣）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>同一次漏抓&lt;strong>常常兩者都有&lt;/strong> —— 要分別處理、不能只修一邊。&lt;/p>
&lt;hr>
&lt;h2 id="具體-case">具體 case&lt;/h2>
&lt;p>這次 review 一篇技術教材、跑了「3 個臨時擬的 agent frame + 一次字句層 grep」、回報「字句層大致 clean」。結果使用者分多輪 catch 出 register/stance 類問題（安撫讀者 / 第二人稱 / 自評誇飾 / 必然框架）。用 WRAP Consider the Opposite 反向檢驗，發現&lt;strong>兩種 gap 都有&lt;/strong>：&lt;/p>
&lt;h3 id="execution-gap我沒跑完整框架">Execution gap（我沒跑完整框架）&lt;/h3>
&lt;p>multi-pass review 框架定義了輪 9（reader simulation）、輪 10（self-criticism），但這次&lt;strong>我根本沒跑&lt;/strong> —— 只跑了我臨時擬的 3 個 agent frame + grep。如果跑了輪 9（用讀者視角讀「這段在跟我說話嗎」），喊話類當場會 catch。這部分&lt;strong>不能怪框架&lt;/strong>、是我跑了子集。&lt;/p>
&lt;h3 id="design-gap框架本身的-frame-不夠">Design gap（框架本身的 frame 不夠）&lt;/h3>
&lt;p>但即使跑了輪 9、現有定義是「拿掉 code block 看論述能不能 parse」（聚焦&lt;strong>自包含性&lt;/strong>）、&lt;strong>沒有 register lens&lt;/strong>（這段在管理 / 評價 / 絕對化讀者嗎）。而且 register 類有兩個結構特性讓 keyword bank（輪 8）抓不到：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>無穩定關鍵詞&lt;/strong>：第二人稱「你」誤命中 code 註解、祈使句式發散、訴諸群體形式多。&lt;/li>
&lt;li>&lt;strong>最依賴外部 cold-read&lt;/strong>：這次正是**使用者（外部讀者）**才抓到 —— 同一 reviewer 模擬讀者視角有限（per multi-pass 自承的 partial fix 限制）。&lt;/li>
&lt;/ul>
&lt;p>這部分&lt;strong>要改框架&lt;/strong>：輪 9 擴入 register lens、標明 register 類「reader-sim 主、keyword bank 輔」。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="只改框架framework-bloatexecution-習慣沒解">只改框架：framework bloat、execution 習慣沒解&lt;/h3>
&lt;p>漏抓後直覺反應是「加 keyword / 加 frame」—— 因為這看得見、像進步。但若真正成因是 execution gap（沒跑輪 9）、加再多 keyword 都沒用（那些輪還是沒跑）。框架越加越胖、reviewer 還是偷跑子集。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡是一次 review 失誤的 self-retrospective（用 WRAP 的 Consider the Opposite 反向檢驗自己的 review 過程）抽出。具體限制：</p>
<ul>
<li><strong>樣本是 1 次 review、1 個 reviewer（我自己）</strong>：「design gap vs execution gap」這個二分基於單次自我檢討、不是跨多次 review 觀察到的 systematic pattern。</li>
<li><strong>「自我歸因」有 self-serving 風險</strong>：reviewer 檢討自己漏抓時、可能傾向把責任推給「框架不足」（design gap）而非「我偷懶」（execution gap）—— 本卡的價值正是強迫拆開這兩者、但拆的人就是當事人、客觀性有限。</li>
<li><strong>修法有效性未驗證</strong>：診斷後的修法（改框架 vs 改執行）是否真能讓下次 catch 到、未經後續 review 驗證。</li>
</ul>
<p>讀者使用本卡時、把它當「review 失誤歸因的一個檢查步驟」、不當「驗證過的 review 流程」。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p>Review 漏抓某類問題時，先分清是 <strong>design gap</strong> 還是 <strong>execution gap</strong>，再決定修法 —— 兩者修法相反。</p>
<ul>
<li><strong>Design gap</strong>：框架根本沒有對應的 frame / keyword 去 catch 這類問題。修法是<strong>改框架</strong>（補 frame、補 keyword bank、補 lens）。</li>
<li><strong>Execution gap</strong>：框架有對應 frame、但 reviewer 這次沒跑（跑了子集、跳過該跑的輪）。修法是<strong>改執行</strong>（真的跑完該跑的輪），改框架沒用。</li>
</ul>
<table>
  <thead>
      <tr>
          <th>成因</th>
          <th>問題在哪</th>
          <th>修法</th>
          <th>誤判的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Design gap</td>
          <td>框架缺 frame</td>
          <td>補 frame / keyword / lens</td>
          <td>誤判成 execution → 一直漏同類（以為「下次認真跑」就好、但根本沒對應 frame）</td>
      </tr>
      <tr>
          <td>Execution gap</td>
          <td>沒跑該跑的輪</td>
          <td>跑完整框架</td>
          <td>誤判成 design → framework bloat（一直加 frame、卻沒解決「偷跑子集」的習慣）</td>
      </tr>
  </tbody>
</table>
<p>同一次漏抓<strong>常常兩者都有</strong> —— 要分別處理、不能只修一邊。</p>
<hr>
<h2 id="具體-case">具體 case</h2>
<p>這次 review 一篇技術教材、跑了「3 個臨時擬的 agent frame + 一次字句層 grep」、回報「字句層大致 clean」。結果使用者分多輪 catch 出 register/stance 類問題（安撫讀者 / 第二人稱 / 自評誇飾 / 必然框架）。用 WRAP Consider the Opposite 反向檢驗，發現<strong>兩種 gap 都有</strong>：</p>
<h3 id="execution-gap我沒跑完整框架">Execution gap（我沒跑完整框架）</h3>
<p>multi-pass review 框架定義了輪 9（reader simulation）、輪 10（self-criticism），但這次<strong>我根本沒跑</strong> —— 只跑了我臨時擬的 3 個 agent frame + grep。如果跑了輪 9（用讀者視角讀「這段在跟我說話嗎」），喊話類當場會 catch。這部分<strong>不能怪框架</strong>、是我跑了子集。</p>
<h3 id="design-gap框架本身的-frame-不夠">Design gap（框架本身的 frame 不夠）</h3>
<p>但即使跑了輪 9、現有定義是「拿掉 code block 看論述能不能 parse」（聚焦<strong>自包含性</strong>）、<strong>沒有 register lens</strong>（這段在管理 / 評價 / 絕對化讀者嗎）。而且 register 類有兩個結構特性讓 keyword bank（輪 8）抓不到：</p>
<ul>
<li><strong>無穩定關鍵詞</strong>：第二人稱「你」誤命中 code 註解、祈使句式發散、訴諸群體形式多。</li>
<li><strong>最依賴外部 cold-read</strong>：這次正是**使用者（外部讀者）**才抓到 —— 同一 reviewer 模擬讀者視角有限（per multi-pass 自承的 partial fix 限制）。</li>
</ul>
<p>這部分<strong>要改框架</strong>：輪 9 擴入 register lens、標明 register 類「reader-sim 主、keyword bank 輔」。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="只改框架framework-bloatexecution-習慣沒解">只改框架：framework bloat、execution 習慣沒解</h3>
<p>漏抓後直覺反應是「加 keyword / 加 frame」—— 因為這看得見、像進步。但若真正成因是 execution gap（沒跑輪 9）、加再多 keyword 都沒用（那些輪還是沒跑）。框架越加越胖、reviewer 還是偷跑子集。</p>
<h3 id="只改執行漏掉的整類永遠-catch-不到">只改執行：漏掉的整類永遠 catch 不到</h3>
<p>反過來、若把 design gap 誤判成「我下次認真跑就好」、但框架根本沒有對應 frame —— 認真跑現有的輪也 catch 不到、同類問題每次都漏。</p>
<h3 id="加-keyword是最誘人的假修法">「加 keyword」是最誘人的假修法</h3>
<p>加 keyword bank 條目成本低、立即有「補上了」的感覺。但它只解 design gap 的一個 sub-type（偵測層、且限有穩定關鍵詞的類）。對 execution gap（沒跑輪）、對 register 這種無關鍵詞的 design gap、它都無效 —— 容易讓人以為修好了、其實沒。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點</a></strong>：#114 處理的是 <strong>design gap 的一個面向</strong>（frame 不夠細、抽象規則沒展開成具體訊號）。本卡是上位 —— 在「改 frame 顆粒度」之前、先問「這次漏抓到底是缺 frame（design）還是沒跑 frame（execution）」。#114 預設問題在框架、本卡先驗證這個預設。</li>
<li><strong><a href="../rule-codification-vs-self-audit/">#147 規範化跟自審是兩種認知任務</a></strong>：#147 是 execution 側的 sibling —— 「立了規範 ≠ 自己稿件能辨識」講的就是「有 frame 不等於有跑」。本卡把它一般化成「有 frame 不等於有跑（execution gap）」、並跟「根本沒 frame（design gap）」對立起來。</li>
<li><strong><a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 keyword bank 命中是候選、不是判決</a></strong>：#149 是另一個「兩層別混」的 pattern（偵測 vs 判定）。本卡（design vs execution）跟 #149（偵測 vs 判定）都在拆「review 失效的成因層」—— 修法都依賴先分層、再對症。</li>
<li><strong><a href="../teaching-register-states-not-addresses-reader/">#150-152 教材 register / framing 卡</a></strong>：這三張是本卡的觸發 case（漏抓的具體內容）。本卡是它們揭露的 <strong>review 流程層</strong> 教訓 —— 內容卡講「該怎麼寫」、本卡講「review 為什麼沒抓到、該改框架還是改執行」。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Review 漏抓、直覺想「加 keyword / 加 frame」</td>
          <td>先停 —— 問「這次有跑完該跑的輪嗎」、是 execution gap 就別加 frame</td>
      </tr>
      <tr>
          <td>「我下次認真跑就好」</td>
          <td>檢查框架真有對應 frame 嗎 —— 沒有就是 design gap、認真跑也沒用</td>
      </tr>
      <tr>
          <td>跑的是「臨時擬的子集」而非完整框架</td>
          <td>execution gap 訊號 —— 先補跑完整輪、再判斷框架夠不夠</td>
      </tr>
      <tr>
          <td>漏抓的類別有「無穩定關鍵詞」特性</td>
          <td>keyword bank 解不了（design gap 的 reader-sim 類）—— 加 lens、不是加 keyword</td>
      </tr>
      <tr>
          <td>漏抓由外部讀者 / 使用者 catch、自己多輪沒抓到</td>
          <td>該類高度依賴 external cold-read —— 同 reviewer 模擬有限、標明依賴</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：review 流程的 retrospective（漏抓後檢討）、framework 演進決策（要不要加 frame / keyword）、self-review 品質檢討。</li>
<li><strong>不適用</strong>：一次性短文 review（沒有「框架」可言、談不上 design vs execution）。</li>
<li><strong>邊界</strong>：兩者不互斥、常同時存在 —— 本卡要的不是「二選一」、是「分別診斷、分別修」。誤把它當 either-or 會只修一半。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於對 <a href="../../work-log/dart_hof_typedef_readability/">HOF / typedef 文章</a> 的 review 失誤做 WRAP 檢討。使用者問「多輪審查設定是否需要調整」，WRAP 的 Consider the Opposite 步驟揭露：失敗不全是框架缺陷 —— 一半是我<strong>只跑了臨時子集、跳過框架既有的輪 9/10</strong>（execution gap）、一半是<strong>輪 9 定義聚焦自包含性、缺 register lens、且 register 類無穩定關鍵詞</strong>（design gap）。</p>
<p>對應本卡：若沒做這個拆分、最可能的反應是「再加幾個 keyword」（只解 design gap 的偵測 sub-type）、既沒解 execution gap（下次還是偷跑子集）、也沒解 register 的 reader-sim design gap。拆開後修法清楚：execution gap 靠「review 時真的跑完該跑的輪」（紀律、不改框架）、design gap 靠「輪 9 擴 register lens + 標明 external-reader 依賴」（改框架、下一步執行）。</p>
]]></content:encoded></item><item><title>教材的『重點 / 總結』段是內容發散的訊號、該重組正文不該補丁</title><link>https://tarrragon.github.io/blog/report/summary-section-signals-scattered-prose/</link><pubDate>Fri, 05 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/summary-section-signals-scattered-prose/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述源於 &lt;strong>1 個觸發 case&lt;/strong>（&lt;a href="../../work-log/git_stash_untracked/">git stash &lt;code>-u&lt;/code> 筆記&lt;/a>的 review、使用者指出尾端「重點」段沒有必要）、再以 &lt;strong>1 次 &lt;code>content/&lt;/code> 全站掃描&lt;/strong>（309 個總結型段落、Go 模組 53 章樣本、見下方「blog 樣本掃描」段）驗證與擴充。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>「重點段」是這次出現的形式、不是唯一形式&lt;/strong>：同類補丁還有「一句話總結」「TL;DR」「小結」「劃重點」。判準不在標題名稱、在「這段是否只是重述前文已講過的內容」。&lt;/li>
&lt;li>&lt;strong>不是所有 summary 都是補丁&lt;/strong>：長篇 / 多章模組的導覽型 summary（串連各章、給路由）有獨立價值、不適用本卡。本卡針對的是「單篇短文尾端重述自己」這種發散善後。&lt;/li>
&lt;li>&lt;strong>修補有效性未獨立驗證&lt;/strong>：刪掉總結段後讀起來更聚焦、但「有沒有總結段」跟「讀者實際記憶留存」之間沒做使用者測試。&lt;/li>
&lt;/ul>
&lt;p>讀者使用本卡時、先判斷該 summary 段是「重述前文」還是「跨段路由 / 導覽」—— 前者套用、後者保留。&lt;/p>
&lt;hr>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&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;/ul>
&lt;p>兩種結果都指向「不該留總結段」。總結段唯一看似合理的存在理由（怕讀者沒記住），恰好是正文設計不佳的徵兆 —— 用補丁掩蓋了該修的結構。&lt;/p>
&lt;hr>
&lt;h2 id="處理總結段內容先分提醒-vs-概念">處理總結段內容：先分「提醒 vs 概念」&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>純提醒 / 喊話&lt;/td>
 &lt;td>「開新功能要養成順手加 &lt;code>-u&lt;/code> 的習慣」「記得回頭確認」&lt;/td>
 &lt;td>刪 —— 提醒不傳遞新概念、讀者需要時自己回前文&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>有概念價值&lt;/td>
 &lt;td>「為何 Git 預設不收 untracked：因為常是編譯產物、log」&lt;/td>
 &lt;td>併回正文 —— 移到它本該所屬的概念段、一次講清&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>純重述&lt;/td>
 &lt;td>把表格 / 流程再用文字講一遍&lt;/td>
 &lt;td>刪 —— 跟前文重複、是發散的直接證據&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵動作是「&lt;strong>併回&lt;/strong>」而非「丟棄」：總結段裡常混著真正屬於正文卻被擠到尾端的概念（例如某設計選擇的理由）。把它移回該概念出現的段落、既刪了補丁、又補強了正文的「核心原則先行」。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="補丁掩蓋結構問題發散持續累積">補丁掩蓋結構問題、發散持續累積&lt;/h3>
&lt;p>留著總結段、等於接受「正文可以發散、尾端再收」。下一篇繼續發散、繼續補總結、文章組織能力不會進步。總結段是止痛藥、不是治療 —— 它讓「正文沒講清」這個真問題不痛、於是沒人去修。&lt;/p>
&lt;h3 id="讀者付兩次成本讀同一概念">讀者付兩次成本讀同一概念&lt;/h3>
&lt;p>重述型總結讓讀者讀第二遍已經懂的內容。懂的人覺得冗、沒懂的人靠重述也補不上（重述不會比正文講得更清）。兩種讀者都沒得到淨價值、只多付了 token。&lt;/p>
&lt;h3 id="概念價值被埋在補丁裡正文反而缺角">概念價值被埋在補丁裡、正文反而缺角&lt;/h3>
&lt;p>總結段常混著本該在正文的概念（設計理由、邊界條件）。塞在尾端、正文對應位置反而缺了這塊 —— 讀者讀到正文時概念不完整、要讀到最後才補上。這違反「核心原則先行」、把該前置的概念後置成總結。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../compose-feature-at-source-layer/">#64 Feature 操作要跟 Source 同層合成&lt;/a>&lt;/strong>：本卡是該原則在寫作層的同構。#64 講「filter / 操作要在 source 同層或更上游做、不在下游補」、本卡講「概念要在正文（source）講清、不在尾端總結（下游）補」。兩者共享「修在源頭、不在下游打補丁」的骨架 —— 總結段就是寫作版的 post-source patch。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../teaching-register-states-not-addresses-reader/">#150 教材用中性陳述、不對讀者喊話&lt;/a>&lt;/strong>：兩卡常在同一段命中、但軸不同。#150 是&lt;strong>字句層 / stance&lt;/strong>（「養成習慣」是祈使喊話）、本卡是&lt;strong>結構層&lt;/strong>（整個總結段是組織補丁）。一個總結段可能同時是 #150 的喊話載體跟本卡的結構補丁 —— 修法也不同：#150 改句子 register、本卡刪整段並重組正文。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../teaching-gives-reasons-not-quality-verdicts/">#151 教材給技術理由、不替方案下品質評價&lt;/a>&lt;/strong>：兩卡都在問「這段內容該不該留在教材」。#151 砍的是「自評誇飾」（不傳遞概念的品質 verdict）、本卡砍的是「重述總結」（不傳遞新概念的結構補丁）。共同判準：不貢獻新概念的內容、不論它讀起來多像收尾、都是負擔。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../review-miss-diagnose-design-vs-execution-gap/">#153 Review 漏抓先分 design gap 與 execution gap&lt;/a>&lt;/strong>：本卡的「刪總結 vs 重組正文」是 diagnose 先於修法的同類動作。看到總結段別直接刪了事 —— 先用「刪掉正文站不站得住」診斷：站得住是冗餘（execution-side、刪即可）、站不住是正文發散（design-side、要重組）。診斷錯就會「刪了總結、發散的正文還在」。&lt;/li>
&lt;/ul>
&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>文章尾端有「重點 / 小結 / 一句話總結 / TL;DR」段&lt;/td>
 &lt;td>先問「這段是重述前文、還是跨段路由」—— 重述就是補丁候選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>總結段每一條都能在前文找到對應&lt;/td>
 &lt;td>純重述、刪 —— 讀者需要時回前文即可&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪掉總結段後正文仍完整&lt;/td>
 &lt;td>證明總結冗餘、刪是淨減負擔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>刪掉總結段後正文「少了關鍵概念」&lt;/td>
 &lt;td>不是總結要保、是那個概念該併回正文對應段落&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>總結段混著「養成習慣 / 記得 / 別忘了」&lt;/td>
 &lt;td>純提醒、刪 —— 提醒不是教材的責任（連 &lt;a href="../teaching-register-states-not-addresses-reader/">#150&lt;/a>）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自問「沒有這段、讀者會看不懂嗎」答不會&lt;/td>
 &lt;td>補丁確認 —— 刪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>總結段是「一段重述 + 一句下一章路由」&lt;/td>
 &lt;td>混合型 —— 外科式切重述段、留路由句、別整段刪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一模組每章都有結構雷同的小結&lt;/td>
 &lt;td>系統性補丁 —— 在模組層級統一定義小結責任、別逐章判&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：單篇文章 / 卡片尾端「重述自己」的總結段 —— 重點、小結、一句話總結、劃重點、TL;DR。這些地方若只是把前文再講一遍、就是發散善後。&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：跨章模組 / 長篇的&lt;strong>導覽型 summary&lt;/strong>（串連各章、給下一步路由、列模組地圖）—— 它傳遞「結構關係」這個正文沒有的新資訊、不是重述。&lt;code>report/&lt;/code> 的 &lt;code>_index.md&lt;/code> 路由段、教學模組的章節導覽都屬此類。&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：判別線是「這段傳遞&lt;strong>新資訊（含結構 / 路由關係）&lt;/strong>、還是&lt;strong>重述已講過的內容&lt;/strong>」。前者保留、後者刪。長度不是判準 —— 短文也可能有合理的路由段、長文也可能塞冗餘總結。&lt;/li>
&lt;li>&lt;strong>位置反轉的例外：前置摘要不是尾端重述&lt;/strong>：放在文首的摘要（frontmatter &lt;code>description&lt;/code>、開頭一段 abstract）跟尾端重述性質相反 —— 它服務「讀者決定要不要讀、先建立全局框架」、在讀者&lt;strong>還沒讀正文時&lt;/strong>提供導航價值。尾端重述則是在讀者&lt;strong>讀完之後&lt;/strong>把已知內容再講一次、無導航功能。同樣一句「本文講 X 的三個取捨」、放文首是合理摘要、放文尾就是重述補丁。判別不只看「是否重述」、還看「相對正文的位置決定它服務的是導航還是回顧」。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="blog-樣本掃描三種變體與外科式修法">blog 樣本掃描：三種變體與外科式修法&lt;/h2>
&lt;p>對 &lt;code>content/&lt;/code> 掃「小結 / 總結 / 結論」當標題的段落、309 個檔案命中。Go 教學模組是密度最高的樣本：53 章每章一個 &lt;code>## 小結&lt;/code>、但只有 10 個含「下一章」路由、其餘 43 個是純重述章節核心原則。這不是偶發、是模組層級的系統性習慣 —— 也讓「總結段=補丁」這個判讀有了規模證據。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述源於 <strong>1 個觸發 case</strong>（<a href="../../work-log/git_stash_untracked/">git stash <code>-u</code> 筆記</a>的 review、使用者指出尾端「重點」段沒有必要）、再以 <strong>1 次 <code>content/</code> 全站掃描</strong>（309 個總結型段落、Go 模組 53 章樣本、見下方「blog 樣本掃描」段）驗證與擴充。具體限制：</p>
<ul>
<li><strong>「重點段」是這次出現的形式、不是唯一形式</strong>：同類補丁還有「一句話總結」「TL;DR」「小結」「劃重點」。判準不在標題名稱、在「這段是否只是重述前文已講過的內容」。</li>
<li><strong>不是所有 summary 都是補丁</strong>：長篇 / 多章模組的導覽型 summary（串連各章、給路由）有獨立價值、不適用本卡。本卡針對的是「單篇短文尾端重述自己」這種發散善後。</li>
<li><strong>修補有效性未獨立驗證</strong>：刪掉總結段後讀起來更聚焦、但「有沒有總結段」跟「讀者實際記憶留存」之間沒做使用者測試。</li>
</ul>
<p>讀者使用本卡時、先判斷該 summary 段是「重述前文」還是「跨段路由 / 導覽」—— 前者套用、後者保留。</p>
<hr>
<h2 id="核心原則">核心原則</h2>
<p><strong>教材把概念講清楚的責任落在正文、不落在尾端的總結段。</strong> 若一篇文章要靠「重點」段重述才能讓讀者記住、那訊號不是「需要總結」、是正文發散 —— 概念沒在它該出現的位置一次講清、被攤散在各段、所以尾端要再收一次。正確的回應是重組正文、不是補一個總結段替發散善後。</p>
<p>判準很簡單：<strong>刪掉總結段、看正文站不站得住。</strong></p>
<ul>
<li>站得住 → 總結段本就冗餘、刪掉是淨減負擔。</li>
<li>站不住 → 問題在正文組織、該重拆段落、不是靠總結救。</li>
</ul>
<p>兩種結果都指向「不該留總結段」。總結段唯一看似合理的存在理由（怕讀者沒記住），恰好是正文設計不佳的徵兆 —— 用補丁掩蓋了該修的結構。</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>「開新功能要養成順手加 <code>-u</code> 的習慣」「記得回頭確認」</td>
          <td>刪 —— 提醒不傳遞新概念、讀者需要時自己回前文</td>
      </tr>
      <tr>
          <td>有概念價值</td>
          <td>「為何 Git 預設不收 untracked：因為常是編譯產物、log」</td>
          <td>併回正文 —— 移到它本該所屬的概念段、一次講清</td>
      </tr>
      <tr>
          <td>純重述</td>
          <td>把表格 / 流程再用文字講一遍</td>
          <td>刪 —— 跟前文重複、是發散的直接證據</td>
      </tr>
  </tbody>
</table>
<p>關鍵動作是「<strong>併回</strong>」而非「丟棄」：總結段裡常混著真正屬於正文卻被擠到尾端的概念（例如某設計選擇的理由）。把它移回該概念出現的段落、既刪了補丁、又補強了正文的「核心原則先行」。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="補丁掩蓋結構問題發散持續累積">補丁掩蓋結構問題、發散持續累積</h3>
<p>留著總結段、等於接受「正文可以發散、尾端再收」。下一篇繼續發散、繼續補總結、文章組織能力不會進步。總結段是止痛藥、不是治療 —— 它讓「正文沒講清」這個真問題不痛、於是沒人去修。</p>
<h3 id="讀者付兩次成本讀同一概念">讀者付兩次成本讀同一概念</h3>
<p>重述型總結讓讀者讀第二遍已經懂的內容。懂的人覺得冗、沒懂的人靠重述也補不上（重述不會比正文講得更清）。兩種讀者都沒得到淨價值、只多付了 token。</p>
<h3 id="概念價值被埋在補丁裡正文反而缺角">概念價值被埋在補丁裡、正文反而缺角</h3>
<p>總結段常混著本該在正文的概念（設計理由、邊界條件）。塞在尾端、正文對應位置反而缺了這塊 —— 讀者讀到正文時概念不完整、要讀到最後才補上。這違反「核心原則先行」、把該前置的概念後置成總結。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../compose-feature-at-source-layer/">#64 Feature 操作要跟 Source 同層合成</a></strong>：本卡是該原則在寫作層的同構。#64 講「filter / 操作要在 source 同層或更上游做、不在下游補」、本卡講「概念要在正文（source）講清、不在尾端總結（下游）補」。兩者共享「修在源頭、不在下游打補丁」的骨架 —— 總結段就是寫作版的 post-source patch。</li>
<li><strong><a href="../teaching-register-states-not-addresses-reader/">#150 教材用中性陳述、不對讀者喊話</a></strong>：兩卡常在同一段命中、但軸不同。#150 是<strong>字句層 / stance</strong>（「養成習慣」是祈使喊話）、本卡是<strong>結構層</strong>（整個總結段是組織補丁）。一個總結段可能同時是 #150 的喊話載體跟本卡的結構補丁 —— 修法也不同：#150 改句子 register、本卡刪整段並重組正文。</li>
<li><strong><a href="../teaching-gives-reasons-not-quality-verdicts/">#151 教材給技術理由、不替方案下品質評價</a></strong>：兩卡都在問「這段內容該不該留在教材」。#151 砍的是「自評誇飾」（不傳遞概念的品質 verdict）、本卡砍的是「重述總結」（不傳遞新概念的結構補丁）。共同判準：不貢獻新概念的內容、不論它讀起來多像收尾、都是負擔。</li>
<li><strong><a href="../review-miss-diagnose-design-vs-execution-gap/">#153 Review 漏抓先分 design gap 與 execution gap</a></strong>：本卡的「刪總結 vs 重組正文」是 diagnose 先於修法的同類動作。看到總結段別直接刪了事 —— 先用「刪掉正文站不站得住」診斷：站得住是冗餘（execution-side、刪即可）、站不住是正文發散（design-side、要重組）。診斷錯就會「刪了總結、發散的正文還在」。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>文章尾端有「重點 / 小結 / 一句話總結 / TL;DR」段</td>
          <td>先問「這段是重述前文、還是跨段路由」—— 重述就是補丁候選</td>
      </tr>
      <tr>
          <td>總結段每一條都能在前文找到對應</td>
          <td>純重述、刪 —— 讀者需要時回前文即可</td>
      </tr>
      <tr>
          <td>刪掉總結段後正文仍完整</td>
          <td>證明總結冗餘、刪是淨減負擔</td>
      </tr>
      <tr>
          <td>刪掉總結段後正文「少了關鍵概念」</td>
          <td>不是總結要保、是那個概念該併回正文對應段落</td>
      </tr>
      <tr>
          <td>總結段混著「養成習慣 / 記得 / 別忘了」</td>
          <td>純提醒、刪 —— 提醒不是教材的責任（連 <a href="../teaching-register-states-not-addresses-reader/">#150</a>）</td>
      </tr>
      <tr>
          <td>自問「沒有這段、讀者會看不懂嗎」答不會</td>
          <td>補丁確認 —— 刪</td>
      </tr>
      <tr>
          <td>總結段是「一段重述 + 一句下一章路由」</td>
          <td>混合型 —— 外科式切重述段、留路由句、別整段刪</td>
      </tr>
      <tr>
          <td>同一模組每章都有結構雷同的小結</td>
          <td>系統性補丁 —— 在模組層級統一定義小結責任、別逐章判</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：單篇文章 / 卡片尾端「重述自己」的總結段 —— 重點、小結、一句話總結、劃重點、TL;DR。這些地方若只是把前文再講一遍、就是發散善後。</li>
<li><strong>不適用</strong>：跨章模組 / 長篇的<strong>導覽型 summary</strong>（串連各章、給下一步路由、列模組地圖）—— 它傳遞「結構關係」這個正文沒有的新資訊、不是重述。<code>report/</code> 的 <code>_index.md</code> 路由段、教學模組的章節導覽都屬此類。</li>
<li><strong>邊界</strong>：判別線是「這段傳遞<strong>新資訊（含結構 / 路由關係）</strong>、還是<strong>重述已講過的內容</strong>」。前者保留、後者刪。長度不是判準 —— 短文也可能有合理的路由段、長文也可能塞冗餘總結。</li>
<li><strong>位置反轉的例外：前置摘要不是尾端重述</strong>：放在文首的摘要（frontmatter <code>description</code>、開頭一段 abstract）跟尾端重述性質相反 —— 它服務「讀者決定要不要讀、先建立全局框架」、在讀者<strong>還沒讀正文時</strong>提供導航價值。尾端重述則是在讀者<strong>讀完之後</strong>把已知內容再講一次、無導航功能。同樣一句「本文講 X 的三個取捨」、放文首是合理摘要、放文尾就是重述補丁。判別不只看「是否重述」、還看「相對正文的位置決定它服務的是導航還是回顧」。</li>
</ul>
<hr>
<h2 id="blog-樣本掃描三種變體與外科式修法">blog 樣本掃描：三種變體與外科式修法</h2>
<p>對 <code>content/</code> 掃「小結 / 總結 / 結論」當標題的段落、309 個檔案命中。Go 教學模組是密度最高的樣本：53 章每章一個 <code>## 小結</code>、但只有 10 個含「下一章」路由、其餘 43 個是純重述章節核心原則。這不是偶發、是模組層級的系統性習慣 —— 也讓「總結段=補丁」這個判讀有了規模證據。</p>
<p>樣本分成三種變體、各自修法不同：</p>
<table>
  <thead>
      <tr>
          <th>變體</th>
          <th>真實樣本</th>
          <th>性質</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純重述型</td>
          <td><code>channel.md</code>「channel 的重點是資料流、同步點與所有權邊界」、<code>interfaces.md</code>「Go interface 的核心是用行為定義依賴」</td>
          <td>整段重述正文已建立的核心原則、無路由、無新資訊</td>
          <td>刪或重組正文（本卡典型對象）</td>
      </tr>
      <tr>
          <td>重述 + 路由混合型</td>
          <td><code>control-flow.md</code>：一段重述（控制流程刻意維持少量語法）+ 一句路由（下一章回到 package）</td>
          <td>一段補丁包著一句合理路由</td>
          <td>外科式：切重述段、留路由句</td>
      </tr>
      <tr>
          <td>綜合關係型</td>
          <td><code>llm/application-protocols.md</code>「Function calling 是模型能力、structured output 是 sampling 約束、MCP 是 server 協議 —— 三者層級不同、組合使用而非競爭」</td>
          <td>把正文分散講過的多個概念並置成一個關係 frame</td>
          <td>語意判定：正文若從未在一句並置這個關係、屬新結構資訊、保留</td>
      </tr>
  </tbody>
</table>
<p>兩個從樣本提煉的補充、修正了「整段刪」的過度簡化：</p>
<ul>
<li>
<p><strong>真實世界最常見的是混合型、不是純補丁</strong>：一段重述包著一句路由。對混合型套用本卡、修法是<strong>外科式</strong>（切重述、留路由 / 綜合關係）、不是整段刪。判別線下沉到「段內每一句」、不是「整段留或刪」。</p>
</li>
<li>
<p><strong>系統性出現時在模組層級統一決定、不逐章補丁</strong>：當整個模組每章都有小結（Go 53 章），逐章判斷會反覆做同一決定。該在模組層級先定義「小結的責任」（只放跨章路由 / 跨章綜合、不放單章重述）、再一次套到全模組 —— 否則就是本卡反對的「為發散補補丁」在模組尺度重演。</p>
<blockquote>
<p><strong>升格條件（候選獨立卡）</strong>：「smell 系統性出現 → 在模組 / 政策層級統一修、不逐實例補丁」這條、語意上正交於「總結=發散」、且可泛化到 cadence 同骨化（<a href="../cadence-homogenization-in-batch-writing/">#122</a>）、emoji、任何系統性寫作 smell —— 是 <a href="../compose-feature-at-source-layer/">#64</a>「修在 source」往「修法尺度要匹配問題尺度」的泛化。目前只有 <strong>1 個 instance</strong>（Go 小結）、按 <a href="../two-occurrence-threshold/">#42 兩次門檻</a>暫不抽卡（避免過早抽象 / framework bloat）。當第二個系統性寫作 smell 出現同樣「逐實例 vs 模組政策」張力時、即有 2 instance、抽成獨立卡。</p></blockquote>
</li>
<li>
<p><strong>綜合關係型是導覽型例外的近親、但要語意驗證</strong>：「三者層級不同」這種並置、若正文從未在同一處講過這個關係、就是新資訊（傳遞結構）、不是重述。判別問「這句陳述的關係、正文有沒有在某段一次講過」—— 沒有則保留、有則仍是重述。</p>
</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於 review <a href="../../work-log/git_stash_untracked/">git stash <code>-u</code> 筆記</a> 時、由使用者指出尾端「重點」段沒有必要：</p>
<blockquote>
<p>重點這一塊是沒有必要的、如果讀者需要他可以自己回頭確認、不用預設讀者需要重新提醒。如果我們的文章一定要寫重點才能讓讀者記住、表示我們的文章內容太過發散、這時候要考慮的是重新拆分組織文章內容、而不是為了自己文章的設計不佳又多補一個重點章節。</p></blockquote>
<p>該「重點」段有三條：</p>
<ol>
<li><strong>「開新功能幾乎一定有 untracked 檔案、要養成順手加 <code>-u</code> 的習慣」</strong> —— 純提醒（養成習慣）、且跟「問題情境」段已講的重複。刪。</li>
<li><strong>「為何 Git 預設不收 untracked」</strong> —— 有概念價值、解釋了 <code>-u</code> 為何要明確表態。併回「<code>-u</code> 是什麼」段、放在表格之後、讓設計意圖在概念位置一次講清。</li>
<li><strong>「記憶法：<code>-u</code> = untracked、<code>-a</code> = …」</strong> —— 跟表格重複的重述。刪、把 <code>-a</code> 的範圍差異併進前述設計意圖段。</li>
</ol>
<p>刪掉「重點」段後、正文（問題情境 → <code>-u</code> 是什麼含設計意圖 → 正確流程 → 替代做法取捨）仍完整、且因為概念併回正文反而更聚焦 —— 證明總結段本就是補丁、不是必要結構。</p>
<p>對應本卡：總結段的存在價值（怕讀者沒記住）恰好是正文發散的徵兆 —— 修法不是保留總結、是重組正文、把該前置的概念前置、該刪的提醒刪掉。</p>
]]></content:encoded></item><item><title>引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移</title><link>https://tarrragon.github.io/blog/report/reference-by-semantic-title-not-number/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/reference-by-semantic-title-not-number/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>跨段落、跨檔引用一個結構單位（章節、階段、條列項、輪次）時、&lt;strong>引用它的語意標題、不引用它的位置編號&lt;/strong>。「見核心問題」「在操作盤點階段確認」是穩定引用；「見 Stage 3」「Stage 1-3 出現訊號時」是把當下的排列順序寫死進引用點。&lt;/p>
&lt;p>編號可以存在、但只承擔兩個角色：當下閱讀的排序導覽（條列 1、2、3）、跟發布方凍結過的外部 contract（RFC 段號、法條條號）。活文件（會演進的規範、skill、教材、設計文件）的編號是&lt;strong>結構排列的 derivation&lt;/strong> — 插入一個新章節、所有後續編號全部位移、散落各處的引用點卻不會跟著動。&lt;/p>
&lt;p>這條原則同時定義了標題的責任：&lt;strong>每個結構單位都要有說明核心意義的語意標題、讓引用有東西可錨&lt;/strong>。只有編號沒有語意標題的章節（「Stage 3」「第五章」）、等於強迫所有引用者使用會漂移的錨點。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼編號引用會-silent-失效">為什麼編號引用會 silent 失效&lt;/h2>
&lt;h3 id="編號是-derivation引用是複本">編號是 derivation、引用是複本&lt;/h3>
&lt;p>一個章節的編號由「它前面有幾個章節」決定 — 這是排列的衍生值、不是這個章節自身的事實。把「Stage 3」寫進另一個檔案的引用句、等於把這個 derivation 的快照複製出去當 fact 用；結構一變、所有快照同時過期、而且沒有任何機制通知它們過期了。&lt;/p>
&lt;h3 id="失效模式比-broken-link-更糟misdirected不是-dangling">失效模式比 broken link 更糟：misdirected、不是 dangling&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失效類型&lt;/th>
 &lt;th>觸發&lt;/th>
 &lt;th>偵測難度&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Broken link（dangling）&lt;/td>
 &lt;td>目標消失&lt;/td>
 &lt;td>工具可掃：link checker 直接報 404&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編號錯位（misdirected）&lt;/td>
 &lt;td>目標位移、編號被新內容占據&lt;/td>
 &lt;td>工具掃不出來：「Stage 3」字面依然存在、只是指向了別的階段&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>連結斷掉會報錯；編號錯位會&lt;strong>成功解析到錯的內容&lt;/strong>。讀者照著「見 Stage 3」翻過去、看到的是一個真實存在、語意卻完全不同的階段 — 他沒有任何訊號知道引用已經過期。修復也只能靠人工逐處判讀：grep 找得到所有「Stage」字面、但無法判斷哪些語意還對、哪些已經錯位。&lt;/p>
&lt;h3 id="語意標題也會改名--兩種斷裂不同級">語意標題也會改名 — 兩種斷裂不同級&lt;/h3>
&lt;p>語意錨的最強反例是「標題也會改名、改名後所有語意引用一樣過期」。這個攻擊成立一半 — 引用確實會過期、但斷裂的型態與偵測條件完全不同。改名讓舊引用斷成 dangling：「見核心問題」找不到目標、grep 與讀者都判得出引用過期了；編號位移斷成 misdirected：成功解析到錯的內容、無人察覺。更關鍵的差異是操作者在場與否 — 改名是對目標單位的顯式操作、改名者當下就知道該掃引用面、修復可以發生在同一個 commit；編號位移是別處插入章節的副作用、被位移的章節沒有人碰過、它的引用過期沒有任何人在場負責。語意錨輸在「也會變」、贏在「變的時候可偵測、有人在場」。&lt;/p>
&lt;h3 id="時間維度的累積">時間維度的累積&lt;/h3>
&lt;p>寫下引用的當下、編號跟語意完全對得上 — 這正是這個反模式難以自察的原因。漂移發生在後續版本：插入新階段、合併兩章、把一節搬到另一個檔案。文件越活躍、引用點越多、每次重排的人工修復面就越大；漏修一處、就埋下一個 misdirected 引用等讀者踩。&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>「見 Stage 3」「進 Stage 4 前確認」&lt;/td>
 &lt;td>「見核心問題」「進維度展開前確認」— 用階段的語意名稱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「如第 3 點所述」「上述 1-3」&lt;/td>
 &lt;td>重述該點的語意：「如『底線告知協議』所述」「上述三個收斂判準」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「詳見第五章」（活文件）&lt;/td>
 &lt;td>「詳見『防護底線清單』章」— 標題即錨點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>章節只有編號、沒有語意標題（「## Stage 3」）&lt;/td>
 &lt;td>編號與語意並列（「## Stage 3：核心問題」）、引用時取語意半邊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>條列項被遠處引用（「套用流程的步驟 2」）&lt;/td>
 &lt;td>給該步驟一個名字、或引用時帶語意（「套用流程的『抽 findings』步」）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>寫作時的判斷次序：先給結構單位一個承載核心意義的標題（這是標題的本職）、編號只作當下排序；引用時一律取語意名稱；引用點離目標越遠（跨檔、跨 surface）、越是只能用語意錨。&lt;/p>
&lt;h3 id="邊界凍結編號是-fact可以引用">邊界：凍結編號是 fact、可以引用&lt;/h3>
&lt;p>發布方承諾編號不變的外部規格、編號本身就是 contract：RFC 的 section number、法條的條號、含版本年份的 ISO 條款（如 ISO 9001:2015 — 跨版改版會重編條款、引用時鎖定版本才算凍結）、已出版書籍的章節。這些編號被凍結成 fact、引用它們反而比引用標題穩定（標題可能在翻譯與再版間變動）。內部活文件想獲得同樣性質、要先付出同樣代價：宣告編號凍結、只加不改 — 多數活文件不值得也做不到、所以預設走語意引用。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">#156 集合命名用角色、不內嵌數量&lt;/a>：命名端 sibling。本卡假設語意標題是穩定 fact、#156 負責讓這個假設成立 — 「核心七問」這種 count-bearing 標題一半是語意、一半是成員數的 derivation、引用端怎麼修都錨在會漂移的字串上。本卡初版的正面範例就用了「見核心七問」而未察覺、由 #156 抓出 — 兩卡是不同認知層的檢查、單獨跑任一個抓不到另一個的違規。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth&lt;/a>：本卡是 SSoT 在「結構引用」維度的實例。編號是位置的 derivation；把編號寫進多處引用點、等於把 derivation 當 fact 散寫多份複本 — fact（章節的語意身分）只在標題一處、引用就該錨在那裡。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/url-slug-must-be-explicit-fact/" data-link-title="URL slug 必須顯式定義為 fact：跨工具 identifier 用單一定義源" data-link-desc="URL slug 在 Hugo 預設下從 title 自動推導、在 mdtools lint 下從檔名讀、在跨檔連結時又要寫第三個值 — 一個 identifier 散落在三個推導鏈、典型 SSoT 違反。當多個工具共用一個 identifier、推導不一致 = silent broken link。修法：把 slug 從 derivation（runtime 推導）升級成 fact（frontmatter 顯式定義）、檔名 / 連結都基於這個 fact。本卡是 #44 在 toolchain integration 情境的具體實例、是 #82 字面 vs 行為在 identifier 維度的展現。">#93 URL slug 必須顯式定義為 fact&lt;/a>：同屬「引用要錨在 fact」家族。#93 把跨工具 identifier 從推導值升級成顯式 fact；本卡把跨段落引用的錨點從位置推導值（編號）換成語意 fact（標題）。兩卡的失效模式同型：推導鏈分歧、silent 失效、compile / lint 階段看不出來。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/naming-as-iterated-artifact/" data-link-title="Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂" data-link-desc="命名（變數 / 函式 / 檔名 / slug / API endpoint）幾乎沒有「一次寫對」的可能：第一個名字基於當下狹窄的 context、會在後續 cross-call-site / grep / 重構中暴露錯位。命名的正確設計是 iterated — 寫第一版 → grep-ability 測試 → cross-call-site 一致性 → impl 洩漏 → 重命名。本卡是 #83 在「命名」場景的特化。">#84 Naming 是 iterated artifact&lt;/a>：標題是名字。本卡要求標題承載可被引用的語意、等於對標題套用 #84 的 cross-call-site 檢驗 — 從引用者的角度看、這個標題單獨出現時讀者知道它指什麼嗎？只有編號的標題在這個檢驗下直接不及格。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍&lt;/a>：引用句屬於 #97 分類中的 navigation surface（跟 link label、索引條目同層）— 同樣是正文之外、卻直接決定讀者入口正確性的層。重排結構時、review 範圍要把散落各檔的引用句列入掃描面、而不是只改目標檔。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>設計一個多階段訪談 skill 時、初版流程四階段、各檔用「Stage 1 的核心七問」「Stage 3 收斂時」互相引用。下一版把流程改成六階段：操作盤點與領域切分插入在前、核心七問從 Stage 1 變成 Stage 3、決策收斂從 Stage 3 變成 Stage 5。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>跨段落、跨檔引用一個結構單位（章節、階段、條列項、輪次）時、<strong>引用它的語意標題、不引用它的位置編號</strong>。「見核心問題」「在操作盤點階段確認」是穩定引用；「見 Stage 3」「Stage 1-3 出現訊號時」是把當下的排列順序寫死進引用點。</p>
<p>編號可以存在、但只承擔兩個角色：當下閱讀的排序導覽（條列 1、2、3）、跟發布方凍結過的外部 contract（RFC 段號、法條條號）。活文件（會演進的規範、skill、教材、設計文件）的編號是<strong>結構排列的 derivation</strong> — 插入一個新章節、所有後續編號全部位移、散落各處的引用點卻不會跟著動。</p>
<p>這條原則同時定義了標題的責任：<strong>每個結構單位都要有說明核心意義的語意標題、讓引用有東西可錨</strong>。只有編號沒有語意標題的章節（「Stage 3」「第五章」）、等於強迫所有引用者使用會漂移的錨點。</p>
<hr>
<h2 id="為什麼編號引用會-silent-失效">為什麼編號引用會 silent 失效</h2>
<h3 id="編號是-derivation引用是複本">編號是 derivation、引用是複本</h3>
<p>一個章節的編號由「它前面有幾個章節」決定 — 這是排列的衍生值、不是這個章節自身的事實。把「Stage 3」寫進另一個檔案的引用句、等於把這個 derivation 的快照複製出去當 fact 用；結構一變、所有快照同時過期、而且沒有任何機制通知它們過期了。</p>
<h3 id="失效模式比-broken-link-更糟misdirected不是-dangling">失效模式比 broken link 更糟：misdirected、不是 dangling</h3>
<table>
  <thead>
      <tr>
          <th>失效類型</th>
          <th>觸發</th>
          <th>偵測難度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Broken link（dangling）</td>
          <td>目標消失</td>
          <td>工具可掃：link checker 直接報 404</td>
      </tr>
      <tr>
          <td>編號錯位（misdirected）</td>
          <td>目標位移、編號被新內容占據</td>
          <td>工具掃不出來：「Stage 3」字面依然存在、只是指向了別的階段</td>
      </tr>
  </tbody>
</table>
<p>連結斷掉會報錯；編號錯位會<strong>成功解析到錯的內容</strong>。讀者照著「見 Stage 3」翻過去、看到的是一個真實存在、語意卻完全不同的階段 — 他沒有任何訊號知道引用已經過期。修復也只能靠人工逐處判讀：grep 找得到所有「Stage」字面、但無法判斷哪些語意還對、哪些已經錯位。</p>
<h3 id="語意標題也會改名--兩種斷裂不同級">語意標題也會改名 — 兩種斷裂不同級</h3>
<p>語意錨的最強反例是「標題也會改名、改名後所有語意引用一樣過期」。這個攻擊成立一半 — 引用確實會過期、但斷裂的型態與偵測條件完全不同。改名讓舊引用斷成 dangling：「見核心問題」找不到目標、grep 與讀者都判得出引用過期了；編號位移斷成 misdirected：成功解析到錯的內容、無人察覺。更關鍵的差異是操作者在場與否 — 改名是對目標單位的顯式操作、改名者當下就知道該掃引用面、修復可以發生在同一個 commit；編號位移是別處插入章節的副作用、被位移的章節沒有人碰過、它的引用過期沒有任何人在場負責。語意錨輸在「也會變」、贏在「變的時候可偵測、有人在場」。</p>
<h3 id="時間維度的累積">時間維度的累積</h3>
<p>寫下引用的當下、編號跟語意完全對得上 — 這正是這個反模式難以自察的原因。漂移發生在後續版本：插入新階段、合併兩章、把一節搬到另一個檔案。文件越活躍、引用點越多、每次重排的人工修復面就越大；漏修一處、就埋下一個 misdirected 引用等讀者踩。</p>
<hr>
<h2 id="反模式與修法">反模式與修法</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「見 Stage 3」「進 Stage 4 前確認」</td>
          <td>「見核心問題」「進維度展開前確認」— 用階段的語意名稱</td>
      </tr>
      <tr>
          <td>「如第 3 點所述」「上述 1-3」</td>
          <td>重述該點的語意：「如『底線告知協議』所述」「上述三個收斂判準」</td>
      </tr>
      <tr>
          <td>「詳見第五章」（活文件）</td>
          <td>「詳見『防護底線清單』章」— 標題即錨點</td>
      </tr>
      <tr>
          <td>章節只有編號、沒有語意標題（「## Stage 3」）</td>
          <td>編號與語意並列（「## Stage 3：核心問題」）、引用時取語意半邊</td>
      </tr>
      <tr>
          <td>條列項被遠處引用（「套用流程的步驟 2」）</td>
          <td>給該步驟一個名字、或引用時帶語意（「套用流程的『抽 findings』步」）</td>
      </tr>
  </tbody>
</table>
<p>寫作時的判斷次序：先給結構單位一個承載核心意義的標題（這是標題的本職）、編號只作當下排序；引用時一律取語意名稱；引用點離目標越遠（跨檔、跨 surface）、越是只能用語意錨。</p>
<h3 id="邊界凍結編號是-fact可以引用">邊界：凍結編號是 fact、可以引用</h3>
<p>發布方承諾編號不變的外部規格、編號本身就是 contract：RFC 的 section number、法條的條號、含版本年份的 ISO 條款（如 ISO 9001:2015 — 跨版改版會重編條款、引用時鎖定版本才算凍結）、已出版書籍的章節。這些編號被凍結成 fact、引用它們反而比引用標題穩定（標題可能在翻譯與再版間變動）。內部活文件想獲得同樣性質、要先付出同樣代價：宣告編號凍結、只加不改 — 多數活文件不值得也做不到、所以預設走語意引用。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">#156 集合命名用角色、不內嵌數量</a>：命名端 sibling。本卡假設語意標題是穩定 fact、#156 負責讓這個假設成立 — 「核心七問」這種 count-bearing 標題一半是語意、一半是成員數的 derivation、引用端怎麼修都錨在會漂移的字串上。本卡初版的正面範例就用了「見核心七問」而未察覺、由 #156 抓出 — 兩卡是不同認知層的檢查、單獨跑任一個抓不到另一個的違規。</li>
<li><a href="/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth</a>：本卡是 SSoT 在「結構引用」維度的實例。編號是位置的 derivation；把編號寫進多處引用點、等於把 derivation 當 fact 散寫多份複本 — fact（章節的語意身分）只在標題一處、引用就該錨在那裡。</li>
<li><a href="/blog/report/url-slug-must-be-explicit-fact/" data-link-title="URL slug 必須顯式定義為 fact：跨工具 identifier 用單一定義源" data-link-desc="URL slug 在 Hugo 預設下從 title 自動推導、在 mdtools lint 下從檔名讀、在跨檔連結時又要寫第三個值 — 一個 identifier 散落在三個推導鏈、典型 SSoT 違反。當多個工具共用一個 identifier、推導不一致 = silent broken link。修法：把 slug 從 derivation（runtime 推導）升級成 fact（frontmatter 顯式定義）、檔名 / 連結都基於這個 fact。本卡是 #44 在 toolchain integration 情境的具體實例、是 #82 字面 vs 行為在 identifier 維度的展現。">#93 URL slug 必須顯式定義為 fact</a>：同屬「引用要錨在 fact」家族。#93 把跨工具 identifier 從推導值升級成顯式 fact；本卡把跨段落引用的錨點從位置推導值（編號）換成語意 fact（標題）。兩卡的失效模式同型：推導鏈分歧、silent 失效、compile / lint 階段看不出來。</li>
<li><a href="/blog/report/naming-as-iterated-artifact/" data-link-title="Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂" data-link-desc="命名（變數 / 函式 / 檔名 / slug / API endpoint）幾乎沒有「一次寫對」的可能：第一個名字基於當下狹窄的 context、會在後續 cross-call-site / grep / 重構中暴露錯位。命名的正確設計是 iterated — 寫第一版 → grep-ability 測試 → cross-call-site 一致性 → impl 洩漏 → 重命名。本卡是 #83 在「命名」場景的特化。">#84 Naming 是 iterated artifact</a>：標題是名字。本卡要求標題承載可被引用的語意、等於對標題套用 #84 的 cross-call-site 檢驗 — 從引用者的角度看、這個標題單獨出現時讀者知道它指什麼嗎？只有編號的標題在這個檢驗下直接不及格。</li>
<li><a href="/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍</a>：引用句屬於 #97 分類中的 navigation surface（跟 link label、索引條目同層）— 同樣是正文之外、卻直接決定讀者入口正確性的層。重排結構時、review 範圍要把散落各檔的引用句列入掃描面、而不是只改目標檔。</li>
</ul>
<hr>
<h2 id="觸發-case">觸發 case</h2>
<p>設計一個多階段訪談 skill 時、初版流程四階段、各檔用「Stage 1 的核心七問」「Stage 3 收斂時」互相引用。下一版把流程改成六階段：操作盤點與領域切分插入在前、核心七問從 Stage 1 變成 Stage 3、決策收斂從 Stage 3 變成 Stage 5。</p>
<p>後果具體呈現了上述失效模式：十多處跨檔引用要修、grep「Stage」找得到所有字面、但每一處都要人工判讀語意 — 「Stage 3 收斂時」字面完好、語意卻已指向錯誤階段（舊 Stage 3 是收斂、新 Stage 3 是核心七問）。實際修復中就有兩處漏網、靠第二輪全 repo 掃描才補上。若初版引用寫的是「核心問題」「決策收斂階段」這類語意名稱、這次重排的引用修復成本是零。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>寫出「見 Stage N」「如第 N 點」「上一章提過」「§N」且目標是活文件時 — 改用語意標題引用。</li>
<li>章節標題只有編號或編號加泛稱（「階段三」「Part 2」）— 補語意半邊、否則其他文件只能用編號引用該章節。</li>
<li>Review 掃描可用：<code>rg &quot;Stage [0-9]|第 ?[一二三四五六七八九十0-9]+ ?(章|節|點|步|輪)|§[0-9]&quot;</code> — 命中是候選、要逐處判讀目標是凍結編號還是活文件。</li>
<li>結構重排（插入 / 合併 / 搬移章節）的 commit、檢查清單要包含「全 repo 掃引用句」、不是只改目標檔。</li>
</ul>
]]></content:encoded></item><item><title>集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真</title><link>https://tarrragon.github.io/blog/report/name-collections-by-role-not-count/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/name-collections-by-role-not-count/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>集合型結構（問題清單、階段序列、原則組、支柱組）的命名&lt;strong>只承載角色與層級、把成員數量留給清單自己呈現&lt;/strong>。「核心問題」這個名稱承受任意的成員增減；「核心七問」把當前成員數烤進名字、加一問、七就變八：名稱本身先失真、散落各處的引用跟著全部過期。「次要問題」「撞牆階段」「防護底線」同理 — 角色詞完成分層、數量歸清單。&lt;/p>
&lt;p>區分核心問題跟次要問題、靠的是「核心 / 次要」這組角色詞；讀者選擇讀哪一組、不需要先知道組內有幾個成員。數字在名稱裡有真實的讀者側價值 — 記憶掛鉤與完整性提示（「五原則我只想起四個、漏了一個」）— 但這個價值在活集合下被漂移風險壓過、只有凍結集合能無風險收割它、這也是品牌名刻意用數字的原因；路由本身、角色詞已經完成。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼數量入名比編號引用更深一層">為什麼數量入名比編號引用更深一層&lt;/h2>
&lt;h3 id="數量是-membership-的-derivation">數量是 membership 的 derivation&lt;/h3>
&lt;p>一個集合的成員數由「目前有哪些成員」決定 — 跟章節編號由「前面排了幾章」決定同構、都是會隨演進變動的衍生值。差別在寄生位置：編號寄生在&lt;strong>引用句&lt;/strong>、數量寄生在&lt;strong>名稱本身&lt;/strong>。名稱是整個知識系統裡被複製最多次的字串（標題、引用、索引、目錄、對話、commit message 都在複製它）、缺陷跟著名稱繁殖到每一個出現點。&lt;/p>
&lt;h3 id="它讓語意標題是穩定錨的前提失效">它讓「語意標題是穩定錨」的前提失效&lt;/h3>
&lt;p>引用該錨在語意標題、因為標題被假設是這個單位的 fact。「核心七問」打破這個假設：標題有一半是語意（核心問題）、一半是 derivation（七）。成員一變、想守住名實一致就得全面改名、改名又回到「散落引用逐處修」的老路；想省事不改名、就留下一個說謊的名字（標題寫七問、實際有八問）、比編號錯位更糟 — 錯在系統的權威命名層。&lt;/p>
&lt;h3 id="失真的兩條路都有代價">失真的兩條路都有代價&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>成員變動後的選擇&lt;/th>
 &lt;th>代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全面改名&lt;/td>
 &lt;td>名稱出現過的每一處（標題 / 引用 / 索引 / 歷史對話的延續）都要修、等同一次結構重排&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>保留舊名&lt;/td>
 &lt;td>名實不符常態化 — 讀者數了一下發現是八問、開始懷疑文件其他部分的可信度&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>核心七問&lt;/td>
 &lt;td>核心問題&lt;/td>
 &lt;td>角色詞（核心）已完成分層、七是冗餘的快照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成長六階段&lt;/td>
 &lt;td>規模成長撞牆階段&lt;/td>
 &lt;td>階段數會隨教材演進增減&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>四大支柱&lt;/td>
 &lt;td>核心支柱（或直接「支柱」）&lt;/td>
 &lt;td>「大」字結構強迫帶數量、整個棄用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>訪談流程（六階段）&lt;/td>
 &lt;td>訪談流程&lt;/td>
 &lt;td>流程有幾站、目錄自己會說&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>五個定錨問題&lt;/td>
 &lt;td>定錨問題&lt;/td>
 &lt;td>行內描述也一樣、數量讓清單自己呈現&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>寫作時的判斷：命名一個集合時、問「這個數字提供了角色詞沒提供的資訊嗎？」— 答案幾乎都是否。讀者需要知道數量的時刻、是看到清單本身的時刻、清單天然自帶數量。&lt;/p>
&lt;h3 id="邊界三種數字可以留">邊界：三種數字可以留&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>外部凍結的品牌名&lt;/strong>：SOLID 五原則、OWASP Top 10、WRAP 四步驟 — 數量由發布方凍結、是名稱 fact 的一部分、跟引用 RFC 段號同理。&lt;/li>
&lt;li>&lt;strong>數字是概念內容本身&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/report/two-occurrence-threshold/" data-link-title="2 次門檻：第一次是運氣、第二次是訊號" data-link-desc="同一個問題出現第 2 次時、就該停下來把處理層級升一階 — 從推理升到量測、從手動驗證升到自動化、從同方向嘗試升到換思路。第 1 次失敗的資訊不足、第 2 次提供「重複出現」的證據、值得付出升級成本。本文是 #11 / #15 / #20 / #23 四篇實作的共同抽象。">兩次門檻&lt;/a> 的二是規則的閾值、不是成員數 — 改了二就是改了概念、這種數字承載語意、該留。&lt;/li>
&lt;li>&lt;strong>緊鄰清單的行內計數&lt;/strong>：「確認三件事：」後面直接跟三條列 — 數字跟清單同視野、改清單時順手改數字、漂移會立刻被看見。風險低、但仍是小負債、清單就在下面時「確認下列事項：」更省。&lt;/li>
&lt;li>&lt;strong>內部宣告凍結的集合&lt;/strong>：團隊把某個方法論名稱當內部品牌用、可以明文宣告凍結後收割記憶價值 — 代價比凍結編號更嚴：凍結編號還能往後加、凍結數量連加都不能加、成員增減等於名稱重新談判。&lt;/li>
&lt;/ol>
&lt;p>判別線是「這個數字是 fact 還是 derivation」：發布方凍結的、概念內容本身的是 fact；內部活集合的成員數是 derivation、不入名。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">#155 引用章節用語意標題、不用位置編號&lt;/a>：直接 sibling、分工在管線的兩端 — #155 修引用端（錨點選什麼）、本卡修命名端（錨點本身怎麼長）。#155 假設語意標題是穩定 fact、本卡負責讓這個假設成立：count-bearing 標題是「一半 fact 一半 derivation」的混合體、先淨化命名、#155 的引用紀律才有穩定的錨可用。實證：#155 卡初版自己用「見核心七問」當正面範例、修引用端時沒發現錨點內嵌數量。兩卡查的是不同層：引用端的檢查抓不到命名端的缺陷、反過來也一樣。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth&lt;/a>：成員數量的 truth 在清單本身（數一下就有）；把數量寫進名稱是把這個 derivation 複製成第二個源、且這個源被嵌進最高頻複製的字串裡：是 SSoT 違反裡擴散速度最快的形態。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/naming-as-iterated-artifact/" data-link-title="Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂" data-link-desc="命名（變數 / 函式 / 檔名 / slug / API endpoint）幾乎沒有「一次寫對」的可能：第一個名字基於當下狹窄的 context、會在後續 cross-call-site / grep / 重構中暴露錯位。命名的正確設計是 iterated — 寫第一版 → grep-ability 測試 → cross-call-site 一致性 → impl 洩漏 → 重命名。本卡是 #83 在「命名」場景的特化。">#84 Naming 是 iterated artifact&lt;/a>：本卡給 #84 的命名 review 加一個檢查維度 — 數量入名是「第一版命名基於當下狹窄 context」的典型產物：寫名字的當下成員剛好七個、七看起來是這個集合的屬性；cross-time 檢驗（成員會不會變）才暴露它是快照。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">#67 寫作便利度跟意圖對齊反相關&lt;/a>：「七問」「六階段」順口、有節奏感、好記 — 正是便利驅動的選擇；意圖對齊的名稱（核心問題）平淡但誠實。便利的代價延遲到第一次成員變動才結算。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>同一份多階段訪談 skill、同一天內兩次命中：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>集合型結構（問題清單、階段序列、原則組、支柱組）的命名<strong>只承載角色與層級、把成員數量留給清單自己呈現</strong>。「核心問題」這個名稱承受任意的成員增減；「核心七問」把當前成員數烤進名字、加一問、七就變八：名稱本身先失真、散落各處的引用跟著全部過期。「次要問題」「撞牆階段」「防護底線」同理 — 角色詞完成分層、數量歸清單。</p>
<p>區分核心問題跟次要問題、靠的是「核心 / 次要」這組角色詞；讀者選擇讀哪一組、不需要先知道組內有幾個成員。數字在名稱裡有真實的讀者側價值 — 記憶掛鉤與完整性提示（「五原則我只想起四個、漏了一個」）— 但這個價值在活集合下被漂移風險壓過、只有凍結集合能無風險收割它、這也是品牌名刻意用數字的原因；路由本身、角色詞已經完成。</p>
<hr>
<h2 id="為什麼數量入名比編號引用更深一層">為什麼數量入名比編號引用更深一層</h2>
<h3 id="數量是-membership-的-derivation">數量是 membership 的 derivation</h3>
<p>一個集合的成員數由「目前有哪些成員」決定 — 跟章節編號由「前面排了幾章」決定同構、都是會隨演進變動的衍生值。差別在寄生位置：編號寄生在<strong>引用句</strong>、數量寄生在<strong>名稱本身</strong>。名稱是整個知識系統裡被複製最多次的字串（標題、引用、索引、目錄、對話、commit message 都在複製它）、缺陷跟著名稱繁殖到每一個出現點。</p>
<h3 id="它讓語意標題是穩定錨的前提失效">它讓「語意標題是穩定錨」的前提失效</h3>
<p>引用該錨在語意標題、因為標題被假設是這個單位的 fact。「核心七問」打破這個假設：標題有一半是語意（核心問題）、一半是 derivation（七）。成員一變、想守住名實一致就得全面改名、改名又回到「散落引用逐處修」的老路；想省事不改名、就留下一個說謊的名字（標題寫七問、實際有八問）、比編號錯位更糟 — 錯在系統的權威命名層。</p>
<h3 id="失真的兩條路都有代價">失真的兩條路都有代價</h3>
<table>
  <thead>
      <tr>
          <th>成員變動後的選擇</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全面改名</td>
          <td>名稱出現過的每一處（標題 / 引用 / 索引 / 歷史對話的延續）都要修、等同一次結構重排</td>
      </tr>
      <tr>
          <td>保留舊名</td>
          <td>名實不符常態化 — 讀者數了一下發現是八問、開始懷疑文件其他部分的可信度</td>
      </tr>
  </tbody>
</table>
<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>成長六階段</td>
          <td>規模成長撞牆階段</td>
          <td>階段數會隨教材演進增減</td>
      </tr>
      <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>寫作時的判斷：命名一個集合時、問「這個數字提供了角色詞沒提供的資訊嗎？」— 答案幾乎都是否。讀者需要知道數量的時刻、是看到清單本身的時刻、清單天然自帶數量。</p>
<h3 id="邊界三種數字可以留">邊界：三種數字可以留</h3>
<ol>
<li><strong>外部凍結的品牌名</strong>：SOLID 五原則、OWASP Top 10、WRAP 四步驟 — 數量由發布方凍結、是名稱 fact 的一部分、跟引用 RFC 段號同理。</li>
<li><strong>數字是概念內容本身</strong>：<a href="/blog/report/two-occurrence-threshold/" data-link-title="2 次門檻：第一次是運氣、第二次是訊號" data-link-desc="同一個問題出現第 2 次時、就該停下來把處理層級升一階 — 從推理升到量測、從手動驗證升到自動化、從同方向嘗試升到換思路。第 1 次失敗的資訊不足、第 2 次提供「重複出現」的證據、值得付出升級成本。本文是 #11 / #15 / #20 / #23 四篇實作的共同抽象。">兩次門檻</a> 的二是規則的閾值、不是成員數 — 改了二就是改了概念、這種數字承載語意、該留。</li>
<li><strong>緊鄰清單的行內計數</strong>：「確認三件事：」後面直接跟三條列 — 數字跟清單同視野、改清單時順手改數字、漂移會立刻被看見。風險低、但仍是小負債、清單就在下面時「確認下列事項：」更省。</li>
<li><strong>內部宣告凍結的集合</strong>：團隊把某個方法論名稱當內部品牌用、可以明文宣告凍結後收割記憶價值 — 代價比凍結編號更嚴：凍結編號還能往後加、凍結數量連加都不能加、成員增減等於名稱重新談判。</li>
</ol>
<p>判別線是「這個數字是 fact 還是 derivation」：發布方凍結的、概念內容本身的是 fact；內部活集合的成員數是 derivation、不入名。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">#155 引用章節用語意標題、不用位置編號</a>：直接 sibling、分工在管線的兩端 — #155 修引用端（錨點選什麼）、本卡修命名端（錨點本身怎麼長）。#155 假設語意標題是穩定 fact、本卡負責讓這個假設成立：count-bearing 標題是「一半 fact 一半 derivation」的混合體、先淨化命名、#155 的引用紀律才有穩定的錨可用。實證：#155 卡初版自己用「見核心七問」當正面範例、修引用端時沒發現錨點內嵌數量。兩卡查的是不同層：引用端的檢查抓不到命名端的缺陷、反過來也一樣。</li>
<li><a href="/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth</a>：成員數量的 truth 在清單本身（數一下就有）；把數量寫進名稱是把這個 derivation 複製成第二個源、且這個源被嵌進最高頻複製的字串裡：是 SSoT 違反裡擴散速度最快的形態。</li>
<li><a href="/blog/report/naming-as-iterated-artifact/" data-link-title="Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂" data-link-desc="命名（變數 / 函式 / 檔名 / slug / API endpoint）幾乎沒有「一次寫對」的可能：第一個名字基於當下狹窄的 context、會在後續 cross-call-site / grep / 重構中暴露錯位。命名的正確設計是 iterated — 寫第一版 → grep-ability 測試 → cross-call-site 一致性 → impl 洩漏 → 重命名。本卡是 #83 在「命名」場景的特化。">#84 Naming 是 iterated artifact</a>：本卡給 #84 的命名 review 加一個檢查維度 — 數量入名是「第一版命名基於當下狹窄 context」的典型產物：寫名字的當下成員剛好七個、七看起來是這個集合的屬性；cross-time 檢驗（成員會不會變）才暴露它是快照。</li>
<li><a href="/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">#67 寫作便利度跟意圖對齊反相關</a>：「七問」「六階段」順口、有節奏感、好記 — 正是便利驅動的選擇；意圖對齊的名稱（核心問題）平淡但誠實。便利的代價延遲到第一次成員變動才結算。</li>
</ul>
<hr>
<h2 id="觸發-case">觸發 case</h2>
<p>同一份多階段訪談 skill、同一天內兩次命中：</p>
<ol>
<li><strong>已發生</strong>：skill 初版有「四大支柱」、第二版需求調整後支柱變六個 — 名稱被迫改成「六大支柱」、所有提到四大支柱的地方同步修。改完的新名字仍然內嵌數量、下次支柱增減會原樣重演。</li>
<li><strong>被預見</strong>：第二版的「核心七問」「成長六階段」被指出同樣結構 — 核心問題若加一問、「七」就散落在標題、引用、索引、路由表裡等著逐處修。</li>
</ol>
<p>更深的訊號是第三點：當天稍早為了修「Stage 3」編號引用立了一張卡（引用端）、該卡自己用「見核心七問」當正面範例；在專注檢查引用端時、命名端的同型缺陷完全隱形。確認了這是獨立的檢查維度、不是引用卡的子情況。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>命名或標題出現「N 大 X」「N 問」「N 階段」「N 支柱」「N 原則」「N 步驟」且 X 是內部活集合時 — 抽掉數字、看角色詞夠不夠分層、幾乎都夠。</li>
<li>Review 掃描可用：<code>rg &quot;[一二三四五六七八九十0-9]+ ?(大|問|階段|支柱|原則|步驟|件事|個維度)&quot; --type md</code> — 掃出來的不全是病灶：外部凍結品牌與概念閾值是 fact、留；內部活集合的成員數是 derivation、改。</li>
<li>集合成員增減的 commit、檢查名稱是否還誠實 — 需要同步改名的話、這次改名時順便把數量抽掉、別讓下次再來一遍。</li>
</ul>
]]></content:encoded></item><item><title>語意錨用單一字串、同義雙名讓引用修復退回人腦對應</title><link>https://tarrragon.github.io/blog/report/semantic-anchor-single-string/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/semantic-anchor-single-string/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>一個結構單位的語意名稱&lt;strong>只能有一個 canonical 字串、所有引用使用同一字串&lt;/strong>。「決策收斂」跟「決策記錄 + scaffold 建議」都是語意名、都通過了「不用編號」的檢查 — 但兩個字串指同一個階段、語意引用的收益就漏了一半：工具掃描要掃兩個 pattern 才完整、漏配置一個就漏一半引用點；結構重排時、修復者要先在腦中建立「這兩個名字是同一個東西」的對應表、而這張表沒有寫在任何地方。&lt;/p>
&lt;p>語意引用的承諾是「錨點穩定、且機器與人都能單次比對」。同義雙名保住了穩定、丟掉了單次比對 — 等於把 fact 寫成兩種拼法。&lt;/p>
&lt;h2 id="為什麼雙名會自然發生">為什麼雙名會自然發生&lt;/h2>
&lt;p>雙名通常在不同檔案、不同時間寫下：標題在設計流程時取（描述產出物：「決策記錄 + scaffold 建議」）、引用在寫協作檔時取（描述階段角色：「決策收斂階段」）。每一次取名都在自己的當下語境裡合理、分裂要把兩個時間點的產物擺在一起才看得到 — 單檔 review 永遠只看到其中一個名字、所以雙名能安然通過所有單檔檢查。&lt;/p>
&lt;p>偵測需要跨檔視角：把每個結構單位的「標題語意半邊」列成清單、grep 全部引用句、比對引用字串是否落在清單內。引用字串語意對、字面不在清單內、就是雙名。&lt;/p>
&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>標題「Stage 5：決策記錄 + scaffold 建議」、引用「決策收斂階段」&lt;/td>
 &lt;td>二選一、全 repo 統一（標題改「Stage 5：決策收斂（決策記錄 + scaffold 建議）」、引用維持「決策收斂」）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「domain / event 切分」與「領域切分」混用&lt;/td>
 &lt;td>取標題語意半邊為 canonical、其餘出現點改寫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>縮寫與全名並存當引用錨（「ops 階段」「操作維運階段」）&lt;/td>
 &lt;td>全名為 canonical、縮寫只出現在已建立對應的同段內&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>修法的判斷次序：canonical 字串取「標題的語意半邊」（標題是該單位的 fact 所在）；標題語意半邊太長不適合引用時、先改標題、再統一引用 — 標題改短是一次成本、引用各自簡寫是持續發散。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">#155 引用章節用語意標題、不用位置編號&lt;/a>：本卡補 #155 的缺口。#155 把錨點從編號換成語意名稱、預設了「語意名稱是單一的」；雙名讓 #155 的修法只完成一半 — 錨點穩定了、可比對性沒跟上。三卡合起來是完整的引用紀律：#155 管引用端（錨什麼）、#156 管命名端的內容（名稱不含 derivation）、本卡管命名端的唯一性（一個單位一個字串）。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">#156 集合命名用角色、不內嵌數量&lt;/a>：sibling。#156 處理名稱「內容」的 fact 純度、本卡處理名稱「數量」的唯一性 — 一個名稱可以通過 #156（不含成員數）仍違反本卡（有同義變體）。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth&lt;/a>：同一語意身分的兩個字串就是同一個 fact 的兩個源 — 比值的多源更隱蔽、因為兩個字串「語意上相等」、讀者看不出這是違規、只有工具比對才會痛。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/naming-as-iterated-artifact/" data-link-title="Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂" data-link-desc="命名（變數 / 函式 / 檔名 / slug / API endpoint）幾乎沒有「一次寫對」的可能：第一個名字基於當下狹窄的 context、會在後續 cross-call-site / grep / 重構中暴露錯位。命名的正確設計是 iterated — 寫第一版 → grep-ability 測試 → cross-call-site 一致性 → impl 洩漏 → 重命名。本卡是 #83 在「命名」場景的特化。">#84 Naming 是 iterated artifact&lt;/a>：#84 的輪 3 明文檢查「同一個概念在不同檔案用同名嗎」、判讀徵兆也列了「同概念出現兩個以上名字、選一個改另一個」— 本卡是這個檢查項在「結構單位的標題與引用」場景的應用：被檢查的從變數名換成章節 / 階段的語意錨、檢查動作相同。&lt;/li>
&lt;/ul>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>一份多階段訪談 skill 在一致性 audit 中被抓到兩處：階段標題寫「決策記錄 + scaffold 建議」、兩個協作檔的引用寫「決策收斂階段」；另一個階段的引用在「domain / event 切分」與「領域切分」之間混用。每個字串單獨檢查都合規（語意名、無編號、無數量）— audit reviewer 的原話是「語意可對上、但嚴格來說引用錨與標題語意半邊不同字串、下次重排時要靠人腦對應」。修法：標題語意半邊改成「決策收斂」、全 skill 引用統一；「領域切分」全部改寫為「domain / event 切分」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>一個結構單位的語意名稱<strong>只能有一個 canonical 字串、所有引用使用同一字串</strong>。「決策收斂」跟「決策記錄 + scaffold 建議」都是語意名、都通過了「不用編號」的檢查 — 但兩個字串指同一個階段、語意引用的收益就漏了一半：工具掃描要掃兩個 pattern 才完整、漏配置一個就漏一半引用點；結構重排時、修復者要先在腦中建立「這兩個名字是同一個東西」的對應表、而這張表沒有寫在任何地方。</p>
<p>語意引用的承諾是「錨點穩定、且機器與人都能單次比對」。同義雙名保住了穩定、丟掉了單次比對 — 等於把 fact 寫成兩種拼法。</p>
<h2 id="為什麼雙名會自然發生">為什麼雙名會自然發生</h2>
<p>雙名通常在不同檔案、不同時間寫下：標題在設計流程時取（描述產出物：「決策記錄 + scaffold 建議」）、引用在寫協作檔時取（描述階段角色：「決策收斂階段」）。每一次取名都在自己的當下語境裡合理、分裂要把兩個時間點的產物擺在一起才看得到 — 單檔 review 永遠只看到其中一個名字、所以雙名能安然通過所有單檔檢查。</p>
<p>偵測需要跨檔視角：把每個結構單位的「標題語意半邊」列成清單、grep 全部引用句、比對引用字串是否落在清單內。引用字串語意對、字面不在清單內、就是雙名。</p>
<h2 id="反模式與修法">反模式與修法</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>標題「Stage 5：決策記錄 + scaffold 建議」、引用「決策收斂階段」</td>
          <td>二選一、全 repo 統一（標題改「Stage 5：決策收斂（決策記錄 + scaffold 建議）」、引用維持「決策收斂」）</td>
      </tr>
      <tr>
          <td>「domain / event 切分」與「領域切分」混用</td>
          <td>取標題語意半邊為 canonical、其餘出現點改寫</td>
      </tr>
      <tr>
          <td>縮寫與全名並存當引用錨（「ops 階段」「操作維運階段」）</td>
          <td>全名為 canonical、縮寫只出現在已建立對應的同段內</td>
      </tr>
  </tbody>
</table>
<p>修法的判斷次序：canonical 字串取「標題的語意半邊」（標題是該單位的 fact 所在）；標題語意半邊太長不適合引用時、先改標題、再統一引用 — 標題改短是一次成本、引用各自簡寫是持續發散。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">#155 引用章節用語意標題、不用位置編號</a>：本卡補 #155 的缺口。#155 把錨點從編號換成語意名稱、預設了「語意名稱是單一的」；雙名讓 #155 的修法只完成一半 — 錨點穩定了、可比對性沒跟上。三卡合起來是完整的引用紀律：#155 管引用端（錨什麼）、#156 管命名端的內容（名稱不含 derivation）、本卡管命名端的唯一性（一個單位一個字串）。</li>
<li><a href="/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">#156 集合命名用角色、不內嵌數量</a>：sibling。#156 處理名稱「內容」的 fact 純度、本卡處理名稱「數量」的唯一性 — 一個名稱可以通過 #156（不含成員數）仍違反本卡（有同義變體）。</li>
<li><a href="/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth</a>：同一語意身分的兩個字串就是同一個 fact 的兩個源 — 比值的多源更隱蔽、因為兩個字串「語意上相等」、讀者看不出這是違規、只有工具比對才會痛。</li>
<li><a href="/blog/report/naming-as-iterated-artifact/" data-link-title="Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂" data-link-desc="命名（變數 / 函式 / 檔名 / slug / API endpoint）幾乎沒有「一次寫對」的可能：第一個名字基於當下狹窄的 context、會在後續 cross-call-site / grep / 重構中暴露錯位。命名的正確設計是 iterated — 寫第一版 → grep-ability 測試 → cross-call-site 一致性 → impl 洩漏 → 重命名。本卡是 #83 在「命名」場景的特化。">#84 Naming 是 iterated artifact</a>：#84 的輪 3 明文檢查「同一個概念在不同檔案用同名嗎」、判讀徵兆也列了「同概念出現兩個以上名字、選一個改另一個」— 本卡是這個檢查項在「結構單位的標題與引用」場景的應用：被檢查的從變數名換成章節 / 階段的語意錨、檢查動作相同。</li>
</ul>
<h2 id="觸發-case">觸發 case</h2>
<p>一份多階段訪談 skill 在一致性 audit 中被抓到兩處：階段標題寫「決策記錄 + scaffold 建議」、兩個協作檔的引用寫「決策收斂階段」；另一個階段的引用在「domain / event 切分」與「領域切分」之間混用。每個字串單獨檢查都合規（語意名、無編號、無數量）— audit reviewer 的原話是「語意可對上、但嚴格來說引用錨與標題語意半邊不同字串、下次重排時要靠人腦對應」。修法：標題語意半邊改成「決策收斂」、全 skill 引用統一；「領域切分」全部改寫為「domain / event 切分」。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>寫引用句時、回頭看目標標題：引用字串跟標題語意半邊逐字相同嗎？語意相同、字面不同 = 雙名。</li>
<li>跨檔 review 可操作：列出所有結構單位的語意半邊、<code>rg</code> 每個單位的引用句、引用字串不在清單內的逐處判讀。</li>
<li>同單位的引用在不同檔案用不同說法、且沒有任何一處寫明對應 — 雙名已經發生、統一的成本隨引用點數量成長。</li>
</ul>
]]></content:encoded></item><item><title>決策表兩列同時命中且結論相反：缺的是一個上游區分維度</title><link>https://tarrragon.github.io/blog/report/decision-table-conflict-reveals-missing-dimension/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/decision-table-conflict-reveals-missing-dimension/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>決策表的矛盾訊號：&lt;strong>同一個真實案例同時命中兩列、兩列給出相反結論&lt;/strong>。這時最誘人的修法是在表內處理 — 加優先序、加 tie-breaker、把其中一列改窄。比較穩的修法在表外：矛盾通常代表案例承載著兩種身分、而表把兩種身分當成一種在判 — 缺的是一個&lt;strong>上游區分維度&lt;/strong>、補一個前置澄清問把身分拆開、兩列就各自回到自己的適用域、矛盾自然消失。&lt;/p>
&lt;p>判別兩種修法的條件：矛盾案例拆開身分後、兩列的結論各自都對 → 缺上游維度、補澄清問；拆不出身分、兩列就是對同一件事意見相左 → 表內規則真衝突、要改規則本身。&lt;/p>
&lt;h2 id="為什麼逐列檢查抓不到">為什麼逐列檢查抓不到&lt;/h2>
&lt;p>決策表的每一列通常各自正確 — 「產品本身是軟體 → 自建」對、「需求是某平台的標準域 → 用該平台」也對。錯不在任何一列、在兩列的適用域有重疊而設計者沒看到：重疊區的案例會同時觸發兩條路。逐列 review 驗證的是「每列單獨成立嗎」、永遠不會把兩列放進同一個案例裡比。&lt;/p>
&lt;p>偵測需要 dry-run：拿一個真實、具體、帶完整語境的案例（不是為驗證表而造的乾淨例子）、從頭走一遍表、記錄命中了哪些列。乾淨例子只會命中設計者預想的那列；真實案例自帶模糊地帶、才會踩進重疊區。&lt;/p>
&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>表內加優先序（「列 5 優先於反向問」）&lt;/td>
 &lt;td>矛盾被蓋住、重疊區案例被武斷分到一邊、錯一半&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>把其中一列改窄到避開重疊&lt;/td>
 &lt;td>列的語意被扭曲、下一個重疊案例出現時再改窄一次、列越改越難懂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>留給執行者自由心證&lt;/td>
 &lt;td>同一案例不同執行者判出相反結論、表失去存在意義&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>補上游區分維度（前置澄清問）&lt;/strong>&lt;/td>
 &lt;td>重疊區案例先被拆成兩個清晰案例、各自走各自的列&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>修上游維度的具體形式：在表之前加一個澄清問、問題的答案決定案例帶著哪個身分進表。澄清問要放在表前而不是表內 — 它不是表的一列（它沒有結論）、它是表的前提。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Process content 結構由最大差異維度決定&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Data topology 是第 6 audit 維度&lt;/a>：同屬「框架缺維度、用案例暴露」家族。#127/#128 的維度缺口由新類型的 process 案例暴露、本卡的維度缺口由判讀矛盾暴露 — 矛盾是比「套不進」更尖銳的訊號、因為它給出兩個都自信的錯誤答案。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/review-miss-diagnose-design-vs-execution-gap/" data-link-title="Review 漏抓先分 design gap 與 execution gap、再決定改框架還是改執行" data-link-desc="Review 漏抓某類問題時，有兩個不同成因：design gap（框架根本沒有對應 frame）跟 execution gap（框架有 frame、但 reviewer 沒跑）。修法相反 —— design gap 要改框架（補 frame / keyword）、execution gap 要改執行（真的跑完該跑的輪）。診斷前先分清：把 execution gap 誤判成 design gap 會 framework bloat（一直加 frame 卻沒解決偷跑子集）、把 design gap 誤判成 execution gap 會永遠漏同類。常見陷阱是『加 keyword』感覺像進步、但對沒跑的輪毫無幫助。">#153 Review 漏抓先分 design gap 與 execution gap&lt;/a>：#153 立卡的情境是 review 流程的漏抓、本卡把它的診斷分流延伸到決策表設計 — 決策表矛盾是 design gap 的一種具體形態 — 修法是改框架（補維度）、不是改執行（要求執行者更小心地選列）。把矛盾歸因於「執行者判斷力不足」就是 #153 警告的誤診。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/test-first-red-before-green/" data-link-title="Test-First：先看到 RED 才相信 GREEN" data-link-desc="一個只看過 GREEN 的測試是「未驗證的訊號」、不是「會抓回歸的測試」。必須先在「該失敗的版本」上看到 RED、再在「該通過的版本」上看到 GREEN — 兩次跑都對、才能相信測試真的 catch 到該 catch 的東西。跳過 RED 等於把驗收標準降到「跑得通」、漏掉「測試自己有沒有 bug」這層。">#69 Test-First：先看到 RED 才相信 GREEN&lt;/a>：dry-run 真實案例等於給決策表跑測試 — 表設計完只用設計者預想的例子驗證、等於只跑會 GREEN 的測試；真實案例是會 RED 的測試、RED 出現的位置就是重疊區。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/filter-instruction-clarification/" data-link-title="篩選類指令的澄清時機" data-link-desc="「依 X 篩選」這類指令必須先澄清三件事才能寫：定義域（已載入 / 全部 / 子集）、資料分批方式、空狀態的語意。三問跑完才寫、否則必然寫成視覺層 post-filter、撞上 #55 層錯位。">#58 篩選類指令的澄清時機&lt;/a>：前置澄清問的機制同源 — 指令 / 需求在進入執行前先把歧義拆開、成本遠低於執行到一半發現走錯路。&lt;/li>
&lt;/ul>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>一份 SaaS 設計訪談 skill 的「交付形態 gate」有兩條規則：「產品本身是軟體 → 自建」、反向問「核心流程是某平台的首頁文案 → 先用該平台」。讀者旅程 reviewer 用真實案例 dry-run —「做一個給健身教練管理學員課表跟收款的 SaaS」— 兩條同時命中：它是軟體產品（命中前者）、「課表 + 收款」是垂直平台的首頁文案（命中後者）、結論相反、執行者只能自由心證。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>決策表的矛盾訊號：<strong>同一個真實案例同時命中兩列、兩列給出相反結論</strong>。這時最誘人的修法是在表內處理 — 加優先序、加 tie-breaker、把其中一列改窄。比較穩的修法在表外：矛盾通常代表案例承載著兩種身分、而表把兩種身分當成一種在判 — 缺的是一個<strong>上游區分維度</strong>、補一個前置澄清問把身分拆開、兩列就各自回到自己的適用域、矛盾自然消失。</p>
<p>判別兩種修法的條件：矛盾案例拆開身分後、兩列的結論各自都對 → 缺上游維度、補澄清問；拆不出身分、兩列就是對同一件事意見相左 → 表內規則真衝突、要改規則本身。</p>
<h2 id="為什麼逐列檢查抓不到">為什麼逐列檢查抓不到</h2>
<p>決策表的每一列通常各自正確 — 「產品本身是軟體 → 自建」對、「需求是某平台的標準域 → 用該平台」也對。錯不在任何一列、在兩列的適用域有重疊而設計者沒看到：重疊區的案例會同時觸發兩條路。逐列 review 驗證的是「每列單獨成立嗎」、永遠不會把兩列放進同一個案例裡比。</p>
<p>偵測需要 dry-run：拿一個真實、具體、帶完整語境的案例（不是為驗證表而造的乾淨例子）、從頭走一遍表、記錄命中了哪些列。乾淨例子只會命中設計者預想的那列；真實案例自帶模糊地帶、才會踩進重疊區。</p>
<h2 id="反模式與修法">反模式與修法</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>表內加優先序（「列 5 優先於反向問」）</td>
          <td>矛盾被蓋住、重疊區案例被武斷分到一邊、錯一半</td>
      </tr>
      <tr>
          <td>把其中一列改窄到避開重疊</td>
          <td>列的語意被扭曲、下一個重疊案例出現時再改窄一次、列越改越難懂</td>
      </tr>
      <tr>
          <td>留給執行者自由心證</td>
          <td>同一案例不同執行者判出相反結論、表失去存在意義</td>
      </tr>
      <tr>
          <td><strong>補上游區分維度（前置澄清問）</strong></td>
          <td>重疊區案例先被拆成兩個清晰案例、各自走各自的列</td>
      </tr>
  </tbody>
</table>
<p>修上游維度的具體形式：在表之前加一個澄清問、問題的答案決定案例帶著哪個身分進表。澄清問要放在表前而不是表內 — 它不是表的一列（它沒有結論）、它是表的前提。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">#127 Process content 結構由最大差異維度決定</a> 與 <a href="/blog/report/data-topology-as-audit-dimension/" data-link-title="Data topology 是 process content 的第 6 audit 維度" data-link-desc="Process content 的 diff dimension audit 原本 5 維（schema / operational / paradigm / components / application change）漏了 *data topology* — 資料在 cluster / partition / region 之間的分佈拓樸；topology 不在既有 5 維任一個、但決定 re-sharding / partition redesign / multi-region rollout 的結構；本卡擴 audit 到 6 維、新增 Type F「Topology re-layout」結構">#128 Data topology 是第 6 audit 維度</a>：同屬「框架缺維度、用案例暴露」家族。#127/#128 的維度缺口由新類型的 process 案例暴露、本卡的維度缺口由判讀矛盾暴露 — 矛盾是比「套不進」更尖銳的訊號、因為它給出兩個都自信的錯誤答案。</li>
<li><a href="/blog/report/review-miss-diagnose-design-vs-execution-gap/" data-link-title="Review 漏抓先分 design gap 與 execution gap、再決定改框架還是改執行" data-link-desc="Review 漏抓某類問題時，有兩個不同成因：design gap（框架根本沒有對應 frame）跟 execution gap（框架有 frame、但 reviewer 沒跑）。修法相反 —— design gap 要改框架（補 frame / keyword）、execution gap 要改執行（真的跑完該跑的輪）。診斷前先分清：把 execution gap 誤判成 design gap 會 framework bloat（一直加 frame 卻沒解決偷跑子集）、把 design gap 誤判成 execution gap 會永遠漏同類。常見陷阱是『加 keyword』感覺像進步、但對沒跑的輪毫無幫助。">#153 Review 漏抓先分 design gap 與 execution gap</a>：#153 立卡的情境是 review 流程的漏抓、本卡把它的診斷分流延伸到決策表設計 — 決策表矛盾是 design gap 的一種具體形態 — 修法是改框架（補維度）、不是改執行（要求執行者更小心地選列）。把矛盾歸因於「執行者判斷力不足」就是 #153 警告的誤診。</li>
<li><a href="/blog/report/test-first-red-before-green/" data-link-title="Test-First：先看到 RED 才相信 GREEN" data-link-desc="一個只看過 GREEN 的測試是「未驗證的訊號」、不是「會抓回歸的測試」。必須先在「該失敗的版本」上看到 RED、再在「該通過的版本」上看到 GREEN — 兩次跑都對、才能相信測試真的 catch 到該 catch 的東西。跳過 RED 等於把驗收標準降到「跑得通」、漏掉「測試自己有沒有 bug」這層。">#69 Test-First：先看到 RED 才相信 GREEN</a>：dry-run 真實案例等於給決策表跑測試 — 表設計完只用設計者預想的例子驗證、等於只跑會 GREEN 的測試；真實案例是會 RED 的測試、RED 出現的位置就是重疊區。</li>
<li><a href="/blog/report/filter-instruction-clarification/" data-link-title="篩選類指令的澄清時機" data-link-desc="「依 X 篩選」這類指令必須先澄清三件事才能寫：定義域（已載入 / 全部 / 子集）、資料分批方式、空狀態的語意。三問跑完才寫、否則必然寫成視覺層 post-filter、撞上 #55 層錯位。">#58 篩選類指令的澄清時機</a>：前置澄清問的機制同源 — 指令 / 需求在進入執行前先把歧義拆開、成本遠低於執行到一半發現走錯路。</li>
</ul>
<h2 id="觸發-case">觸發 case</h2>
<p>一份 SaaS 設計訪談 skill 的「交付形態 gate」有兩條規則：「產品本身是軟體 → 自建」、反向問「核心流程是某平台的首頁文案 → 先用該平台」。讀者旅程 reviewer 用真實案例 dry-run —「做一個給健身教練管理學員課表跟收款的 SaaS」— 兩條同時命中：它是軟體產品（命中前者）、「課表 + 收款」是垂直平台的首頁文案（命中後者）、結論相反、執行者只能自由心證。</p>
<p>拆身分後矛盾消失：這句需求可以是「要賣給眾多教練的產品」（垂直平台是市場競爭對手、走自建）、也可以是「教練自己管理學員的工具」（垂直平台正是該用的託管形態）。修法是 gate 前加澄清問「這個軟體是要賣的產品、還是經營業務的工具？」— 兩列規則本身一字未改、各自回到正確的適用域。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>設計任何判讀表 / 決策表後、至少用一個真實案例（帶完整語境、不是教科書例）從頭 dry-run、記錄全部命中列。</li>
<li>執行者回報「這個 case 兩條規則都說得通」— 別當成執行者的問題、當成表缺維度的訊號。</li>
<li>修表時發現自己在加優先序或把列改窄 — 停下來問「重疊區的案例是不是承載兩種身分」。</li>
</ul>
]]></content:encoded></item><item><title>入口分流要放在詞彙牆之前、門外讀者要在門外就拿到岔路</title><link>https://tarrragon.github.io/blog/report/audience-fork-before-jargon-wall/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/audience-fork-before-jargon-wall/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>入口頁的開頭屬於最外圈讀者 — 背景最少、最容易跳出的那群人、決定了分流句能放多深。&lt;strong>分流要出現在他們還活著的位置&lt;/strong>：開頭每多一段他們看不懂的內容、活到分流點的機率就掉一截；分流句寫得再清楚、放在詞彙牆後面就等於沒寫、會被牆擋住的讀者正是分流要服務的讀者。&lt;/p>
&lt;p>操作型判準：找出入口頁服務的最外圈讀者、用他的詞彙量重讀開頭 — 第一個他看不懂的術語出現之前、他需要的分流出現了嗎？&lt;/p>
&lt;h2 id="為什麼分流會自然長在牆後">為什麼分流會自然長在牆後&lt;/h2>
&lt;p>入口頁通常由門內的人維護、預設讀者跟自己共享詞彙。為門外讀者補新章節時、自然的動作是「把新章節加進章節列表、在相關段落補一句路由」— 而「相關段落」的位置由內容邏輯決定（交付形態的討論屬於需求討論的前提、所以放在需求討論段前）、不是由讀者存活曲線決定。內容邏輯上正確的位置、可以在讀者旅程上完全失效：邏輯說「這段在第 41 行很合理」、旅程說「目標讀者活不過第 10 行」。&lt;/p>
&lt;p>兩個座標系都對、服從的對象不同 — 入口頁的開頭屬於旅程座標系、內容深處才屬於邏輯座標系。&lt;/p>
&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>分流句放在「邏輯上相關」的段落、不管該段落多深&lt;/td>
 &lt;td>分流句在開頭一、二段內出現、邏輯位置可以放第二份&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>章節列表按編號排、門外讀者的章節排在幾十列門內章節後&lt;/td>
 &lt;td>列表順序維持、但開頭的分流句直接連到該章節、不靠讀者掃列表&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>入口頁開頭堆共同詞彙索引、預設讀者先去補課&lt;/td>
 &lt;td>補課指引放在分流之後 — 先讓每種讀者找到自己的路、再談前置知識&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用「本模組預設 X」的宣告代替分流&lt;/td>
 &lt;td>宣告要配出口：「預設自建已成立；尚未確定的讀者、先讀（連結）」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>位置的選擇可以很粗暴：入口頁前三句之內、每一種讀者都拿得到一個自己看得懂的出口。內容邏輯再怎麼支持別的位置、都排在這個約束後面。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/teaching-completeness-by-learner-journey/" data-link-title="教材完整性要用讀者旅程驗證" data-link-desc="教材完整性要用讀者旅程驗證；章節數、案例數或 vendor 覆蓋度只能判斷素材量。成熟教材要能回答不同讀者從哪裡開始、按什麼順序讀、讀完能做什麼。LLM 與 Go 目錄顯示，讀者旅程、學習梯度與主題導讀是教學設計完成度的核心訊號。">#131 教材完整性要用讀者旅程驗證&lt;/a>：本卡是 #131 在「入口頁」這個單點的特化。#131 驗證整條旅程（從哪開始、按什麼順序、讀完能做什麼）、本卡聚焦旅程的第 0 步 — 旅程設計得再好、入口斷路就沒有人走上來。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/top-level-content-folder-needs-homepage-entry/" data-link-title="新增頂層 content 資料夾要同步首頁 _index.md 入口" data-link-desc="Hugo 不會 auto-list 頂層資料夾、首頁的模組清單是 content/_index.md 的手動 curated markdown。新增 content/&amp;lt;module&amp;gt;/ 後若沒同步加入口、模組在首頁完全不可發現；讀者只能靠搜尋或直接打 URL 才進得去。本卡把『新增頂層資料夾 &amp;#43; 首頁入口』綁成同 commit 的雙生動作、避免下次再漏。是 #44 SSoT 在『首頁清單』維度 &amp;#43; #97 metadata surface 在『上層索引』維度的具體案例。">#139 新增頂層 content 資料夾要同步首頁入口&lt;/a>：同屬「內容存在 ≠ 內容可達」家族。#139 處理結構性不可達（首頁清單沒列、完全找不到）、本卡處理體驗性不可達（列了、但目標讀者活不到那一行）— 後者更隱蔽、因為 link checker 與結構審查都會通過。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍&lt;/a>：#97 的 navigation surface 列舉的是索引條目、MOC hook 與 link label；本卡把入口頁的開頭段視為這個 surface 的延伸 — 這是本卡的擴張、原卡分類未列 — 理由是它跟那批元素共享同一個責任：決定讀者入口、跟正文品質是兩個 review 維度。新章節寫完只 review 章節本身、入口 surface 的失效就漏掉了；本卡的 case 正是被 reader-simulation 而不是內容 review 抓到。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/search-engine-matching-mode-mismatch/" data-link-title="搜尋引擎的匹配模式跟使用者預期的對齊" data-link-desc="搜尋引擎的匹配模式（prefix / substring / fuzzy / semantic）各有不同。預設多半是 prefix（為了 index size）、但使用者被 Google 訓練成預期 substring。沒對齊 = silent 失敗：搜「pre」找不到 backpressure。本卡展開五種匹配模式、跟使用者意圖的對齊協議、五個合成策略。">#73 搜尋引擎的匹配模式跟使用者預期對齊&lt;/a>：同型的「入口錯位 = silent 失敗」— 使用者帶著自己的模型來、系統用另一個模型回應、沒有錯誤訊息、只有流失。&lt;/li>
&lt;/ul>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>一個後端選型模組為「還沒決定要不要自建的讀者」補了交付形態章節（託管平台 / BaaS / 自建的判讀）— 這是整個模組唯一為光譜左端讀者寫的內容。Reader-simulation 審查用人設實走：獨立開發者、剛用低程式碼工具做完接案網站、想知道該不該自建。結果：模組入口頁開頭三段全是自建世界的詞彙（consumer lag、dead-letter queue、replay）、唯一的分流句在第 41 行的「需求討論順序」標題下、章節列表中該章排在第 17 列 — reviewer 的判定是「這個讀者最可能的真實行為：開頁、前兩段每個名詞都不認識、跳出、根本走不到那一章」。修復後的入口頁在第二段就給出分流（值不值得自建、連到該章）、原本第 41 行的路由保留作第二觸點。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>為新讀者群補了內容、commit 裡只有新章節跟列表項、入口頁開頭沒動 — 大概率長在牆後。&lt;/li>
&lt;li>入口頁 review 用門內視角讀（「資訊都在」）— 換最外圈讀者的詞彙量重讀開頭十行、數第一個陌生術語出現的位置。&lt;/li>
&lt;li>分流句的位置由「內容邏輯歸屬」決定而不是「讀者存活範圍」決定 — 兩個座標系打架時、開頭讓給旅程座標系。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>入口頁的開頭屬於最外圈讀者 — 背景最少、最容易跳出的那群人、決定了分流句能放多深。<strong>分流要出現在他們還活著的位置</strong>：開頭每多一段他們看不懂的內容、活到分流點的機率就掉一截；分流句寫得再清楚、放在詞彙牆後面就等於沒寫、會被牆擋住的讀者正是分流要服務的讀者。</p>
<p>操作型判準：找出入口頁服務的最外圈讀者、用他的詞彙量重讀開頭 — 第一個他看不懂的術語出現之前、他需要的分流出現了嗎？</p>
<h2 id="為什麼分流會自然長在牆後">為什麼分流會自然長在牆後</h2>
<p>入口頁通常由門內的人維護、預設讀者跟自己共享詞彙。為門外讀者補新章節時、自然的動作是「把新章節加進章節列表、在相關段落補一句路由」— 而「相關段落」的位置由內容邏輯決定（交付形態的討論屬於需求討論的前提、所以放在需求討論段前）、不是由讀者存活曲線決定。內容邏輯上正確的位置、可以在讀者旅程上完全失效：邏輯說「這段在第 41 行很合理」、旅程說「目標讀者活不過第 10 行」。</p>
<p>兩個座標系都對、服從的對象不同 — 入口頁的開頭屬於旅程座標系、內容深處才屬於邏輯座標系。</p>
<h2 id="反模式與修法">反模式與修法</h2>
<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>用「本模組預設 X」的宣告代替分流</td>
          <td>宣告要配出口：「預設自建已成立；尚未確定的讀者、先讀（連結）」</td>
      </tr>
  </tbody>
</table>
<p>位置的選擇可以很粗暴：入口頁前三句之內、每一種讀者都拿得到一個自己看得懂的出口。內容邏輯再怎麼支持別的位置、都排在這個約束後面。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/teaching-completeness-by-learner-journey/" data-link-title="教材完整性要用讀者旅程驗證" data-link-desc="教材完整性要用讀者旅程驗證；章節數、案例數或 vendor 覆蓋度只能判斷素材量。成熟教材要能回答不同讀者從哪裡開始、按什麼順序讀、讀完能做什麼。LLM 與 Go 目錄顯示，讀者旅程、學習梯度與主題導讀是教學設計完成度的核心訊號。">#131 教材完整性要用讀者旅程驗證</a>：本卡是 #131 在「入口頁」這個單點的特化。#131 驗證整條旅程（從哪開始、按什麼順序、讀完能做什麼）、本卡聚焦旅程的第 0 步 — 旅程設計得再好、入口斷路就沒有人走上來。</li>
<li><a href="/blog/report/top-level-content-folder-needs-homepage-entry/" data-link-title="新增頂層 content 資料夾要同步首頁 _index.md 入口" data-link-desc="Hugo 不會 auto-list 頂層資料夾、首頁的模組清單是 content/_index.md 的手動 curated markdown。新增 content/&lt;module&gt;/ 後若沒同步加入口、模組在首頁完全不可發現；讀者只能靠搜尋或直接打 URL 才進得去。本卡把『新增頂層資料夾 &#43; 首頁入口』綁成同 commit 的雙生動作、避免下次再漏。是 #44 SSoT 在『首頁清單』維度 &#43; #97 metadata surface 在『上層索引』維度的具體案例。">#139 新增頂層 content 資料夾要同步首頁入口</a>：同屬「內容存在 ≠ 內容可達」家族。#139 處理結構性不可達（首頁清單沒列、完全找不到）、本卡處理體驗性不可達（列了、但目標讀者活不到那一行）— 後者更隱蔽、因為 link checker 與結構審查都會通過。</li>
<li><a href="/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍</a>：#97 的 navigation surface 列舉的是索引條目、MOC hook 與 link label；本卡把入口頁的開頭段視為這個 surface 的延伸 — 這是本卡的擴張、原卡分類未列 — 理由是它跟那批元素共享同一個責任：決定讀者入口、跟正文品質是兩個 review 維度。新章節寫完只 review 章節本身、入口 surface 的失效就漏掉了；本卡的 case 正是被 reader-simulation 而不是內容 review 抓到。</li>
<li><a href="/blog/report/search-engine-matching-mode-mismatch/" data-link-title="搜尋引擎的匹配模式跟使用者預期的對齊" data-link-desc="搜尋引擎的匹配模式（prefix / substring / fuzzy / semantic）各有不同。預設多半是 prefix（為了 index size）、但使用者被 Google 訓練成預期 substring。沒對齊 = silent 失敗：搜「pre」找不到 backpressure。本卡展開五種匹配模式、跟使用者意圖的對齊協議、五個合成策略。">#73 搜尋引擎的匹配模式跟使用者預期對齊</a>：同型的「入口錯位 = silent 失敗」— 使用者帶著自己的模型來、系統用另一個模型回應、沒有錯誤訊息、只有流失。</li>
</ul>
<h2 id="觸發-case">觸發 case</h2>
<p>一個後端選型模組為「還沒決定要不要自建的讀者」補了交付形態章節（託管平台 / BaaS / 自建的判讀）— 這是整個模組唯一為光譜左端讀者寫的內容。Reader-simulation 審查用人設實走：獨立開發者、剛用低程式碼工具做完接案網站、想知道該不該自建。結果：模組入口頁開頭三段全是自建世界的詞彙（consumer lag、dead-letter queue、replay）、唯一的分流句在第 41 行的「需求討論順序」標題下、章節列表中該章排在第 17 列 — reviewer 的判定是「這個讀者最可能的真實行為：開頁、前兩段每個名詞都不認識、跳出、根本走不到那一章」。修復後的入口頁在第二段就給出分流（值不值得自建、連到該章）、原本第 41 行的路由保留作第二觸點。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>為新讀者群補了內容、commit 裡只有新章節跟列表項、入口頁開頭沒動 — 大概率長在牆後。</li>
<li>入口頁 review 用門內視角讀（「資訊都在」）— 換最外圈讀者的詞彙量重讀開頭十行、數第一個陌生術語出現的位置。</li>
<li>分流句的位置由「內容邏輯歸屬」決定而不是「讀者存活範圍」決定 — 兩個座標系打架時、開頭讓給旅程座標系。</li>
</ul>
]]></content:encoded></item><item><title>跨 surface 同主題內容要重新語境化、不是搬運：逐字相同句是未語境化的訊號</title><link>https://tarrragon.github.io/blog/report/cross-surface-recontextualize-not-transplant/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cross-surface-recontextualize-not-transplant/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>同主題內容要落在兩個 surface（人類教材 ↔ agent 協議、公開文章 ↔ 內部 skill）時、每一份要&lt;strong>為自己的讀者與用途重寫、不是把句子搬過去&lt;/strong>。「各寫一份」的規範如果用複製貼上執行、得到的是最差組合：兩份字面綁定（改一邊、另一邊變成過期複本、且沒有任何機制提醒）、卻又各自沒有為自己的 surface 最佳化（教材句直接當協議用、agent 拿到的是給人類讀的敘述而不是可執行的指令）。&lt;/p>
&lt;p>語境化的可操作判準：&lt;strong>兩個 surface 之間 grep 逐字相同的完整句、命中就是候選&lt;/strong>。同一個原則、教材版該長成「為什麼 + 案例 + 判讀」、協議版該長成「步驟 + 條件 + 產出格式」— 兩版講同一件事、句子自然長得不一樣；句子一樣、代表至少有一邊在用別人的形狀。&lt;/p>
&lt;h2 id="為什麼搬運是預設行為">為什麼搬運是預設行為&lt;/h2>
&lt;p>寫第二份時、第一份就在手邊、而且它「已經寫對了」— 複製是零成本、重寫要重新思考這個 surface 的讀者需要什麼形狀。搬運在當下看起來甚至更安全（兩邊保證一致）；代價在第一次單邊修改時兌現：修了教材版的措辭、skill 版還是舊句、兩邊開始各說各話、而因為當初沒有宣告同源關係、沒有人知道要同步。&lt;/p>
&lt;p>「禁止跨 surface 引用」防的是顯性耦合；搬運建立的是隱性耦合 — 更糟、因為看不見。語境化重寫讓兩份真正獨立：主旨對應靠概念、不靠字串。&lt;/p>
&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>教材的判讀句逐字進協議（「判讀單位是每條業務流程」）&lt;/td>
 &lt;td>協議版改寫成操作指令（「gate 逐條業務流程過、混合結論寫進交付形態欄」）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反向問的問句兩邊同字&lt;/td>
 &lt;td>教材版是讀者自問、協議版是「請使用者……然後對照」的訪談動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>比喻句 / 警句原樣複製（修辭跨 surface 失效最快）&lt;/td>
 &lt;td>各 surface 用自己的語感重寫、或協議版直接刪修辭留判準&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>表格整張複製&lt;/td>
 &lt;td>欄位依用途裁剪：教材表帶「為什麼」欄、協議表帶「下一步路由」欄&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>欄位與句式跟著用途走之後、兩份的字面自然分開；對照查漏時找的是概念缺口、不是字串差異。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth&lt;/a>：逐字搬運讓同一段論述以兩份完整複本存在、誰是權威沒有任何標記。比一般 SSoT 違反更難修 — 第一步的困難不在改、在發現同源關係存在。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度&lt;/a>：偵測手段同構 — 都是 grep 句子層的字面重複；差別在範圍：#122 抓同 surface 多檔的句式骨架重複、本卡抓跨 surface 的完整句逐字重複。兩個掃描可以同一輪跑。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/rule-codification-vs-self-audit/" data-link-title="規範化跟自審是兩種認知任務、立規範當下無法保護同批稿件" data-link-desc="把反模式抽象成規範卡、跟在自己稿件辨識該反模式的局部實例、是兩種不同認知任務；前者用『歸納共同特徵』的視角、後者用『局部 pattern matching』的視角；用相同概念詞、走不同神經路徑；案例：#146 卡描述「看 X 如何 Y」是反模式、同 batch 5 篇章節仍有 11 處該句型未被作者察覺；修法是規範化當下立刻把規範轉成 grep keyword、對同 batch 稿件主動 sweep；不修則 #122 主題語意 attractor 跟 #124 emergence 違規會在同 batch 內持續累積">#147 規範化跟自審是兩種認知任務&lt;/a>：本卡的 case 正是規範已存在（「各寫一份、語境化在各 surface 內」白紙黑字）、執行時仍搬運 — 立規範的人在執行時用了「各寫一份」的字面（檔案確實是兩個）、漏了「語境化」的實質（句子是同一批）。規範的字面合規與實質合規是兩層檢查。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">#150 教材用中性陳述、不對讀者喊話&lt;/a>：register 是語境化最敏感的維度 — 教材的中性陳述、協議的指令語氣、訪談的問句、三種 register 不可互換；搬運最先破壞的就是 register 對位。&lt;/li>
&lt;/ul>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>一個交付形態判讀主題同時寫進 blog 教學章節與 agent 訪談 skill。Cadence 審查跨 surface 比對、抓到三句完整句逐字相同：「官方首頁文案」反向問、「判讀單位是每條業務流程」、「同一個錯誤的兩個方向」。兩個 surface 的規範明文要求各寫一份語境化 — 檔案確實是兩份、句子卻是搬的。修法把 skill 版三句全部改寫成訪談操作語氣（「請使用者用一句話描述核心流程、然後對照」「gate 逐條業務流程過、混合結論直接寫進決策記錄的交付形態欄」）、修辭句直接刪除 — 改寫後兩邊各自更貼自己的用途、概念對應不變。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>寫第二個 surface 時發現自己在開兩個視窗對照逐段抄 — 關掉第一份、憑概念重寫、寫完再對照查漏。&lt;/li>
&lt;li>跨 surface 掃描可操作：對兩份檔案跑完整句比對（句號切分後 grep 交集）、逐字相同的非術語句逐處判讀。&lt;/li>
&lt;li>兩份同主題內容的 register 相同（教材版讀起來像協議、或協議版讀起來像散文）— 即使句子不同、也是語境化不完整的訊號。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>同主題內容要落在兩個 surface（人類教材 ↔ agent 協議、公開文章 ↔ 內部 skill）時、每一份要<strong>為自己的讀者與用途重寫、不是把句子搬過去</strong>。「各寫一份」的規範如果用複製貼上執行、得到的是最差組合：兩份字面綁定（改一邊、另一邊變成過期複本、且沒有任何機制提醒）、卻又各自沒有為自己的 surface 最佳化（教材句直接當協議用、agent 拿到的是給人類讀的敘述而不是可執行的指令）。</p>
<p>語境化的可操作判準：<strong>兩個 surface 之間 grep 逐字相同的完整句、命中就是候選</strong>。同一個原則、教材版該長成「為什麼 + 案例 + 判讀」、協議版該長成「步驟 + 條件 + 產出格式」— 兩版講同一件事、句子自然長得不一樣；句子一樣、代表至少有一邊在用別人的形狀。</p>
<h2 id="為什麼搬運是預設行為">為什麼搬運是預設行為</h2>
<p>寫第二份時、第一份就在手邊、而且它「已經寫對了」— 複製是零成本、重寫要重新思考這個 surface 的讀者需要什麼形狀。搬運在當下看起來甚至更安全（兩邊保證一致）；代價在第一次單邊修改時兌現：修了教材版的措辭、skill 版還是舊句、兩邊開始各說各話、而因為當初沒有宣告同源關係、沒有人知道要同步。</p>
<p>「禁止跨 surface 引用」防的是顯性耦合；搬運建立的是隱性耦合 — 更糟、因為看不見。語境化重寫讓兩份真正獨立：主旨對應靠概念、不靠字串。</p>
<h2 id="反模式與修法">反模式與修法</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>教材的判讀句逐字進協議（「判讀單位是每條業務流程」）</td>
          <td>協議版改寫成操作指令（「gate 逐條業務流程過、混合結論寫進交付形態欄」）</td>
      </tr>
      <tr>
          <td>反向問的問句兩邊同字</td>
          <td>教材版是讀者自問、協議版是「請使用者……然後對照」的訪談動作</td>
      </tr>
      <tr>
          <td>比喻句 / 警句原樣複製（修辭跨 surface 失效最快）</td>
          <td>各 surface 用自己的語感重寫、或協議版直接刪修辭留判準</td>
      </tr>
      <tr>
          <td>表格整張複製</td>
          <td>欄位依用途裁剪：教材表帶「為什麼」欄、協議表帶「下一步路由」欄</td>
      </tr>
  </tbody>
</table>
<p>欄位與句式跟著用途走之後、兩份的字面自然分開；對照查漏時找的是概念缺口、不是字串差異。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth</a>：逐字搬運讓同一段論述以兩份完整複本存在、誰是權威沒有任何標記。比一般 SSoT 違反更難修 — 第一步的困難不在改、在發現同源關係存在。</li>
<li><a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 Cadence 同質化是模板的隱形維度</a>：偵測手段同構 — 都是 grep 句子層的字面重複；差別在範圍：#122 抓同 surface 多檔的句式骨架重複、本卡抓跨 surface 的完整句逐字重複。兩個掃描可以同一輪跑。</li>
<li><a href="/blog/report/rule-codification-vs-self-audit/" data-link-title="規範化跟自審是兩種認知任務、立規範當下無法保護同批稿件" data-link-desc="把反模式抽象成規範卡、跟在自己稿件辨識該反模式的局部實例、是兩種不同認知任務；前者用『歸納共同特徵』的視角、後者用『局部 pattern matching』的視角；用相同概念詞、走不同神經路徑；案例：#146 卡描述「看 X 如何 Y」是反模式、同 batch 5 篇章節仍有 11 處該句型未被作者察覺；修法是規範化當下立刻把規範轉成 grep keyword、對同 batch 稿件主動 sweep；不修則 #122 主題語意 attractor 跟 #124 emergence 違規會在同 batch 內持續累積">#147 規範化跟自審是兩種認知任務</a>：本卡的 case 正是規範已存在（「各寫一份、語境化在各 surface 內」白紙黑字）、執行時仍搬運 — 立規範的人在執行時用了「各寫一份」的字面（檔案確實是兩個）、漏了「語境化」的實質（句子是同一批）。規範的字面合規與實質合規是兩層檢查。</li>
<li><a href="/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">#150 教材用中性陳述、不對讀者喊話</a>：register 是語境化最敏感的維度 — 教材的中性陳述、協議的指令語氣、訪談的問句、三種 register 不可互換；搬運最先破壞的就是 register 對位。</li>
</ul>
<h2 id="觸發-case">觸發 case</h2>
<p>一個交付形態判讀主題同時寫進 blog 教學章節與 agent 訪談 skill。Cadence 審查跨 surface 比對、抓到三句完整句逐字相同：「官方首頁文案」反向問、「判讀單位是每條業務流程」、「同一個錯誤的兩個方向」。兩個 surface 的規範明文要求各寫一份語境化 — 檔案確實是兩份、句子卻是搬的。修法把 skill 版三句全部改寫成訪談操作語氣（「請使用者用一句話描述核心流程、然後對照」「gate 逐條業務流程過、混合結論直接寫進決策記錄的交付形態欄」）、修辭句直接刪除 — 改寫後兩邊各自更貼自己的用途、概念對應不變。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>寫第二個 surface 時發現自己在開兩個視窗對照逐段抄 — 關掉第一份、憑概念重寫、寫完再對照查漏。</li>
<li>跨 surface 掃描可操作：對兩份檔案跑完整句比對（句號切分後 grep 交集）、逐字相同的非術語句逐處判讀。</li>
<li>兩份同主題內容的 register 相同（教材版讀起來像協議、或協議版讀起來像散文）— 即使句子不同、也是語境化不完整的訊號。</li>
</ul>
]]></content:encoded></item><item><title>摘要壓縮可以丟細節、不可以改模態</title><link>https://tarrragon.github.io/blog/report/summary-compression-preserves-modality/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/summary-compression-preserves-modality/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>讀 description 進來的人以為底線零彈性 — 這個誤解是摘要製造的、本體沒寫過這種規則。摘要、description、索引 hook 對規則做壓縮時、&lt;strong>可以丟細節、不可以改模態&lt;/strong>：模態是約束的強度與結構：禁止（不可做）、條件允許（可做、但要滿足 X）、預設加例外（預設 A、條件 B 時走 C）。「防護底線可延後、但不可沉默跳過、要記錄延後理由與重評條件」是條件允許 — 壓成「不可跳過的防護底線」變成禁止、規則精心設計的出口（記錄式延後）在摘要層消失了。&lt;/p>
&lt;p>檢驗的問題只有一個：&lt;strong>讀者只依摘要行動、會不會做出本體不要求、或錯過本體允許的事？&lt;/strong>「不可跳過」的讀者會以為底線零彈性、可能因此放棄整個流程、或在現實壓力下偷偷跳過 — 兩種行為都是本體設計（給一條光明正大的延後路）想避免的。&lt;/p>
&lt;h2 id="為什麼模態最容易被壓掉">為什麼模態最容易被壓掉&lt;/h2>
&lt;p>摘要追求短、模態詞通常比較長：「不可沉默跳過」比「不可跳過」多兩個字、「可延後但要記錄重評條件」比「必須做」多七個字。壓縮時每個字都在被審視「能不能省」、而模態詞看起來像修飾 — 砍掉之後句子更乾脆、語氣更有力、寫摘要的人甚至會覺得更好。失真就藏在「更有力」裡：力度是模態的一部分、加強力度就是改模態。&lt;/p>
&lt;p>另一個機制：摘要常在本體完成後補寫、寫摘要時人記得的是規則的「主旨」（底線很重要、不能隨便跳）、不是規則的「結構」（重要、而且有一條設計好的延後路徑）。主旨記憶天然丟結構。&lt;/p>
&lt;h2 id="反模式與修法">反模式與修法&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>「不可跳過的防護底線」&lt;/td>
 &lt;td>可延後、不可沉默、要記錄&lt;/td>
 &lt;td>讀者以為零彈性、放棄流程或偷偷跳過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「必須先寫測試」&lt;/td>
 &lt;td>預設先寫、prototype 階段可後補&lt;/td>
 &lt;td>探索性工作被迫套不適用的紀律&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「禁止使用 X」&lt;/td>
 &lt;td>預設避免、場景 Y 經評估可用&lt;/td>
 &lt;td>場景 Y 的合法使用被誤殺、或規則被整體無視&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「自動部署到 production」&lt;/td>
 &lt;td>通過 gate 後自動部署&lt;/td>
 &lt;td>讀者以為無人把關、信任崩壞&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>前三列的失真都往絕對化壓（彈性 → 禁令）、這是主要的重力方向；第四列是反向 — 把有把關講成無把關 — 較少見、但判準同一條：摘要引發的行為偏離本體。&lt;/p>
&lt;p>修法：壓縮時保留模態的最小標記 — 「不可沉默跳過」「預設 X、例外見本體」「通過 gate 後自動」。模態標記是摘要裡優先級最高的字、跟主詞動詞同級、比任何細節都後砍。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍&lt;/a>：本卡是 #97 的「審什麼」具體化之一 — metadata surface 進了 review 範圍之後、模態一致性是該 surface 最該查的維度、因為 description 是讀者第一個（常常也是唯一一個）讀到的版本。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/article-body-must-align-with-title-commitment/" data-link-title="文章主體要對齊標題承諾、WRAP 內部分析不該喧賓奪主" data-link-desc="文章標題對讀者做了承諾、文章主體必須對齊這個承諾。WRAP 內部分析（Widen Options &amp;#43; Reality Test 含 prior 引用 &amp;#43; evidence weight）即使方法論做得好、如果不是標題承諾的內容、就不該佔文章主體—屬於 scope mismatch、跟 process metadata 暴露（#141）的議題分開。附帶議題：當 WRAP 內部分析喧賓奪主、為了支撐 prior 容易引入沒實際出處的 source citation；把 WRAP 內部分析從主體移除、hallucination 風險自然降低。是 #141 的姊妹卡—#141 處理章節標題 surface、本卡處理章節內容 scope。">#142 文章主體要對齊標題承諾&lt;/a>：方向相反的同一條對齊軸 — #142 查 body 有沒有兌現 title 的承諾（由上往下）、本卡查摘要有沒有忠實代表 body 的約束（由下往上）。兩個方向都通過、surface 跟本體才真正一致。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/design-choices-framed-as-choices-not-necessity/" data-link-title="教材把設計選擇講成選擇、不講成必然或天性" data-link-desc="本質主義 / 必然性框架（天生 / 本質就是 / 必然 / 唯一）把一個設計選擇講成自然法則、抹掉設計能動性，讓讀者以為沒得選。它是『機會成本語氣 vs 絕對主義』違反的一個 subtype —— 不是命令式絕對（應該做 X）、而是必然性絕對（X 本來就這樣）、更隱形。sharp feature 是常局部牴觸作者自己在別處的條件性立場。修法是把必然框架還原成條件性：X 在『選了某前提』之後才以此形式成立。邊界：物理 / 法律 / 合規事實可講必然。">#152 教材把設計選擇講成選擇、不講成必然&lt;/a>：跟 #152 同在「模態失真」這條軸上。#152 抓正文把條件性講成必然性、本卡抓摘要把條件允許壓成絕對禁止 — 主要失真方向相同（彈性 → 絕對；反向較少見、見上表第四列）、發生層不同（正文論述 vs 壓縮層）。絕對化的句子在兩層都更省字、更有力、更好寫 — 失真有一致的重力方向。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">#67 寫作便利度跟意圖對齊反相關&lt;/a>：「不可跳過」比「不可沉默跳過」好寫好讀、正是便利驅動 — 摘要層的字數壓力讓這個重力更強、所以摘要的模態審查要比正文更嚴。&lt;/li>
&lt;/ul>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>使用者開啟一個訪談 skill、description 告訴他「每個維度附不可跳過的防護底線」— 他合理推論底線零彈性、可能因此覺得流程太硬。本體寫的是另一回事：「可延後、不可沉默跳過（記錄『已告知 + 延後理由 + 重評條件』）」、整個 baseline reference 的核心設計就是那條延後記錄協議。跨 surface 審查的判定：「body 的設計更精緻、description 的壓縮把它講成絕對禁令」。修復把「不可跳過」改成「不可沉默跳過」— 模態跟著那兩個字一起回來。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>讀 description 進來的人以為底線零彈性 — 這個誤解是摘要製造的、本體沒寫過這種規則。摘要、description、索引 hook 對規則做壓縮時、<strong>可以丟細節、不可以改模態</strong>：模態是約束的強度與結構：禁止（不可做）、條件允許（可做、但要滿足 X）、預設加例外（預設 A、條件 B 時走 C）。「防護底線可延後、但不可沉默跳過、要記錄延後理由與重評條件」是條件允許 — 壓成「不可跳過的防護底線」變成禁止、規則精心設計的出口（記錄式延後）在摘要層消失了。</p>
<p>檢驗的問題只有一個：<strong>讀者只依摘要行動、會不會做出本體不要求、或錯過本體允許的事？</strong>「不可跳過」的讀者會以為底線零彈性、可能因此放棄整個流程、或在現實壓力下偷偷跳過 — 兩種行為都是本體設計（給一條光明正大的延後路）想避免的。</p>
<h2 id="為什麼模態最容易被壓掉">為什麼模態最容易被壓掉</h2>
<p>摘要追求短、模態詞通常比較長：「不可沉默跳過」比「不可跳過」多兩個字、「可延後但要記錄重評條件」比「必須做」多七個字。壓縮時每個字都在被審視「能不能省」、而模態詞看起來像修飾 — 砍掉之後句子更乾脆、語氣更有力、寫摘要的人甚至會覺得更好。失真就藏在「更有力」裡：力度是模態的一部分、加強力度就是改模態。</p>
<p>另一個機制：摘要常在本體完成後補寫、寫摘要時人記得的是規則的「主旨」（底線很重要、不能隨便跳）、不是規則的「結構」（重要、而且有一條設計好的延後路徑）。主旨記憶天然丟結構。</p>
<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>「必須先寫測試」</td>
          <td>預設先寫、prototype 階段可後補</td>
          <td>探索性工作被迫套不適用的紀律</td>
      </tr>
      <tr>
          <td>「禁止使用 X」</td>
          <td>預設避免、場景 Y 經評估可用</td>
          <td>場景 Y 的合法使用被誤殺、或規則被整體無視</td>
      </tr>
      <tr>
          <td>「自動部署到 production」</td>
          <td>通過 gate 後自動部署</td>
          <td>讀者以為無人把關、信任崩壞</td>
      </tr>
  </tbody>
</table>
<p>前三列的失真都往絕對化壓（彈性 → 禁令）、這是主要的重力方向；第四列是反向 — 把有把關講成無把關 — 較少見、但判準同一條：摘要引發的行為偏離本體。</p>
<p>修法：壓縮時保留模態的最小標記 — 「不可沉默跳過」「預設 X、例外見本體」「通過 gate 後自動」。模態標記是摘要裡優先級最高的字、跟主詞動詞同級、比任何細節都後砍。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍</a>：本卡是 #97 的「審什麼」具體化之一 — metadata surface 進了 review 範圍之後、模態一致性是該 surface 最該查的維度、因為 description 是讀者第一個（常常也是唯一一個）讀到的版本。</li>
<li><a href="/blog/report/article-body-must-align-with-title-commitment/" data-link-title="文章主體要對齊標題承諾、WRAP 內部分析不該喧賓奪主" data-link-desc="文章標題對讀者做了承諾、文章主體必須對齊這個承諾。WRAP 內部分析（Widen Options &#43; Reality Test 含 prior 引用 &#43; evidence weight）即使方法論做得好、如果不是標題承諾的內容、就不該佔文章主體—屬於 scope mismatch、跟 process metadata 暴露（#141）的議題分開。附帶議題：當 WRAP 內部分析喧賓奪主、為了支撐 prior 容易引入沒實際出處的 source citation；把 WRAP 內部分析從主體移除、hallucination 風險自然降低。是 #141 的姊妹卡—#141 處理章節標題 surface、本卡處理章節內容 scope。">#142 文章主體要對齊標題承諾</a>：方向相反的同一條對齊軸 — #142 查 body 有沒有兌現 title 的承諾（由上往下）、本卡查摘要有沒有忠實代表 body 的約束（由下往上）。兩個方向都通過、surface 跟本體才真正一致。</li>
<li><a href="/blog/report/design-choices-framed-as-choices-not-necessity/" data-link-title="教材把設計選擇講成選擇、不講成必然或天性" data-link-desc="本質主義 / 必然性框架（天生 / 本質就是 / 必然 / 唯一）把一個設計選擇講成自然法則、抹掉設計能動性，讓讀者以為沒得選。它是『機會成本語氣 vs 絕對主義』違反的一個 subtype —— 不是命令式絕對（應該做 X）、而是必然性絕對（X 本來就這樣）、更隱形。sharp feature 是常局部牴觸作者自己在別處的條件性立場。修法是把必然框架還原成條件性：X 在『選了某前提』之後才以此形式成立。邊界：物理 / 法律 / 合規事實可講必然。">#152 教材把設計選擇講成選擇、不講成必然</a>：跟 #152 同在「模態失真」這條軸上。#152 抓正文把條件性講成必然性、本卡抓摘要把條件允許壓成絕對禁止 — 主要失真方向相同（彈性 → 絕對；反向較少見、見上表第四列）、發生層不同（正文論述 vs 壓縮層）。絕對化的句子在兩層都更省字、更有力、更好寫 — 失真有一致的重力方向。</li>
<li><a href="/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">#67 寫作便利度跟意圖對齊反相關</a>：「不可跳過」比「不可沉默跳過」好寫好讀、正是便利驅動 — 摘要層的字數壓力讓這個重力更強、所以摘要的模態審查要比正文更嚴。</li>
</ul>
<h2 id="觸發-case">觸發 case</h2>
<p>使用者開啟一個訪談 skill、description 告訴他「每個維度附不可跳過的防護底線」— 他合理推論底線零彈性、可能因此覺得流程太硬。本體寫的是另一回事：「可延後、不可沉默跳過（記錄『已告知 + 延後理由 + 重評條件』）」、整個 baseline reference 的核心設計就是那條延後記錄協議。跨 surface 審查的判定：「body 的設計更精緻、description 的壓縮把它講成絕對禁令」。修復把「不可跳過」改成「不可沉默跳過」— 模態跟著那兩個字一起回來。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>寫完 description / hook / 目錄註解、回頭比對本體：摘要裡的每個禁止詞與必須詞、本體是同等強度嗎？本體帶「但 / 除非 / 可…需」的句子、摘要保留出口了嗎？</li>
<li>摘要讀起來比本體「更有力、更乾脆」— 力度差就是模態差的訊號、不是文筆進步。</li>
<li>規則被使用者抱怨「太死」或被整體無視時、先查他讀到的是哪一層 — 常常本體無罪、是摘要把彈性壓掉了。</li>
<li>反方向也要掃一眼：摘要讀起來比本體更寬鬆（「自動」「隨時可」）、把本體的把關與條件吃掉了 — 較少見、同樣是模態失真。</li>
</ul>
]]></content:encoded></item><item><title>引用卡片用被引卡自己的分類詞彙：改寫對方的 taxonomy 是隱性錯引</title><link>https://tarrragon.github.io/blog/report/cite-cards-with-their-own-taxonomy/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cite-cards-with-their-own-taxonomy/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫關係段的第一個動作是把被引卡重新打開 — 因為&lt;strong>描述另一張卡、要用它自己的分類詞彙&lt;/strong>、而記憶給不出 taxonomy 級的細節。「引用句是 metadata surface 的一種」這句話裡、「metadata surface」是被引卡的術語 — 而被引卡自己的分類表把 link label、索引條目歸在與 metadata surface 並列的 &lt;strong>navigation surface&lt;/strong>。引用方憑印象把對方的兩個分類併成一個、關係宣告的精神沒錯、詞彙錯位了。&lt;/p>
&lt;p>錯位的代價由循線的讀者支付：他讀到引用句、點過去想看完整論述、發現被引卡的分類表跟引用句對不上 — 輕則困惑（是我讀錯還是卡寫錯）、重則回頭懷疑引用卡的其他關係宣告是不是也是憑印象寫的。關係段的價值在「可回溯」、詞彙錯位直接打在回溯體驗上。&lt;/p>
&lt;h2 id="為什麼憑印象轉述是預設">為什麼憑印象轉述是預設&lt;/h2>
&lt;p>寫關係段時、被引卡是「自己以前寫的、很熟」— 熟悉感讓人跳過重讀、直接憑記憶描述。而記憶存的是概念（「那張卡講非正文的 surface 要進 review」）、不是分類結構（它把非正文拆成 metadata 跟 navigation 兩類）。概念記憶寫出來的引用、語意大方向對、taxonomy 細節隨機。翻譯憑語感不查原文、換掉的是術語的角色；引用憑記憶不開原卡、換掉的是分類的歸屬 — 兩個介面、同一種失誤。&lt;/p>
&lt;p>被引卡越熟、風險越高：陌生的卡會被打開來查、自己的卡靠記憶。&lt;/p>
&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>「X 是 metadata surface 的一種」（被引卡歸 navigation）&lt;/td>
 &lt;td>重讀被引卡分類段、改用它的詞：「X 屬於該卡分類中的 navigation surface」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>把被引卡的三分法轉述成二分法&lt;/td>
 &lt;td>引用時保留它的分法、自己的簡化標明是簡化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用自己卡的術語替換對方同概念的術語&lt;/td>
 &lt;td>兩套術語並陳（「本卡稱 A、該卡稱 B」）、讓讀者能雙向對照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>引用句的宣稱強度超過被引卡（它說 sibling、引成上位）&lt;/td>
 &lt;td>關係的方向與層級照抄被引卡的自我定位、有分歧就明寫分歧&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>再陳述的查核對象永遠是原文、不是記憶 — 不論那張卡是不是自己寫的、寫關係段前把結論段跟分類表重新攤開。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/translation-must-preserve-concept-role/" data-link-title="術語翻譯要保留概念角色" data-link-desc="術語翻譯不能只追求中文好讀，還要保留原詞在論證中的概念角色。Steelman 若翻成「最強版本測試」，reader 會以為它是一個檢查動作；但在決策語境裡，它更核心的責任是把反方論點重建成最強版本。">#109 術語翻譯要保留概念角色&lt;/a>：同一條紀律在不同介面 — #109 管跨語言轉述（翻譯時概念在原文的角色不能換位）、本卡管跨卡片轉述（引用時概念在被引卡的分類位置不能換位）。兩者的失誤機制相同：轉述者用自己的理解重新組織了對方的結構。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/terminology-keeps-original-anchor/" data-link-title="術語翻譯要保留原文錨點" data-link-desc="翻譯術語時、中文名稱負責降低閱讀門檻，原文名稱負責鎖定概念邊界。只留中文會把 reader 帶進中文詞的日常歧義，只留英文會提高閱讀成本；中文後接英文括號是技術文章的穩定折衷。">#107 術語翻譯要保留原文錨點&lt;/a>：#107 的「保留原文讓概念可回溯」在引用場景的對應物 — 引用句裡被引卡的術語就是錨點、錨點寫錯、回溯就斷。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/fact-vs-derive-citation-layering/" data-link-title="引用案例要分觀察層 / 判讀層、強化詞是錯位訊號" data-link-desc="引用案例（特別是 rich case）時、case 內容分兩層：觀察層（具體 fact）跟判讀層（作者推論）；兩層在章節引用時要分層標明、避免把作者判讀升級成 case fact；強化詞（才是 / 必須 / 一定 / 關鍵是）通常是錯位訊號、保留 case 原文的條件性表述（取決於 / 核心瓶頸 / 主要驅動）">#116 引用案例要分觀察層 / 判讀層&lt;/a>：跟 #116 都在守引用的準確性。#116 管引用內容的層次標註（哪些是 case 的事實、哪些是本文推導）、本卡管引用所用詞彙的歸屬（描述對方時用對方的詞）— 兩者都在防「引用方的加工被讀者當成被引方的原文」。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍&lt;/a>：本卡的觸發 case 恰好是引用 #97 時錯置了它的分類 — fact-check reviewer 打開 #97 的分類表逐欄比對才抓到。關係段的 review 方法就是這個動作：逐條開被引卡核對。&lt;/li>
&lt;/ul>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>一張新卡的關係段寫「引用句是 metadata surface 的一種 — 它不是正文論述、是指向結構的導航層」、引用對象是「Metadata surface 要納入寫作 review 範圍」卡。Fact-check 審查打開被引卡、發現它的分類表把 link label / 索引條目列在 &lt;strong>navigation surface&lt;/strong>、跟 metadata surface 是並列的兩列 — 引用句把對方明確分開的兩類併成了一類。判定原文：「關係精神成立（非正文 surface 要納入 review 掃描面）、但嚴格按被引卡的詞彙、引用句更接近 navigation surface」。修法：引用句改成「引用句屬於該卡分類中的 navigation surface（跟 link label、索引條目同層）」— 關係宣告不變、詞彙歸位。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>寫關係段時沒有重新打開被引卡 — 不論多熟、開來把結論段跟分類表掃一遍再寫。&lt;/li>
&lt;li>引用句使用了被引卡的專有分類詞（surface 類型、層級名、模式名）— 逐詞回原卡確認歸屬、這些詞是對方的 taxonomy、不是公共詞彙。&lt;/li>
&lt;li>review 關係段的方法：每條關係宣告配一次「開被引卡、找到支撐句」的核對、找不到支撐句的宣告就是憑印象寫的。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫關係段的第一個動作是把被引卡重新打開 — 因為<strong>描述另一張卡、要用它自己的分類詞彙</strong>、而記憶給不出 taxonomy 級的細節。「引用句是 metadata surface 的一種」這句話裡、「metadata surface」是被引卡的術語 — 而被引卡自己的分類表把 link label、索引條目歸在與 metadata surface 並列的 <strong>navigation surface</strong>。引用方憑印象把對方的兩個分類併成一個、關係宣告的精神沒錯、詞彙錯位了。</p>
<p>錯位的代價由循線的讀者支付：他讀到引用句、點過去想看完整論述、發現被引卡的分類表跟引用句對不上 — 輕則困惑（是我讀錯還是卡寫錯）、重則回頭懷疑引用卡的其他關係宣告是不是也是憑印象寫的。關係段的價值在「可回溯」、詞彙錯位直接打在回溯體驗上。</p>
<h2 id="為什麼憑印象轉述是預設">為什麼憑印象轉述是預設</h2>
<p>寫關係段時、被引卡是「自己以前寫的、很熟」— 熟悉感讓人跳過重讀、直接憑記憶描述。而記憶存的是概念（「那張卡講非正文的 surface 要進 review」）、不是分類結構（它把非正文拆成 metadata 跟 navigation 兩類）。概念記憶寫出來的引用、語意大方向對、taxonomy 細節隨機。翻譯憑語感不查原文、換掉的是術語的角色；引用憑記憶不開原卡、換掉的是分類的歸屬 — 兩個介面、同一種失誤。</p>
<p>被引卡越熟、風險越高：陌生的卡會被打開來查、自己的卡靠記憶。</p>
<h2 id="反模式與修法">反模式與修法</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「X 是 metadata surface 的一種」（被引卡歸 navigation）</td>
          <td>重讀被引卡分類段、改用它的詞：「X 屬於該卡分類中的 navigation surface」</td>
      </tr>
      <tr>
          <td>把被引卡的三分法轉述成二分法</td>
          <td>引用時保留它的分法、自己的簡化標明是簡化</td>
      </tr>
      <tr>
          <td>用自己卡的術語替換對方同概念的術語</td>
          <td>兩套術語並陳（「本卡稱 A、該卡稱 B」）、讓讀者能雙向對照</td>
      </tr>
      <tr>
          <td>引用句的宣稱強度超過被引卡（它說 sibling、引成上位）</td>
          <td>關係的方向與層級照抄被引卡的自我定位、有分歧就明寫分歧</td>
      </tr>
  </tbody>
</table>
<p>再陳述的查核對象永遠是原文、不是記憶 — 不論那張卡是不是自己寫的、寫關係段前把結論段跟分類表重新攤開。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/translation-must-preserve-concept-role/" data-link-title="術語翻譯要保留概念角色" data-link-desc="術語翻譯不能只追求中文好讀，還要保留原詞在論證中的概念角色。Steelman 若翻成「最強版本測試」，reader 會以為它是一個檢查動作；但在決策語境裡，它更核心的責任是把反方論點重建成最強版本。">#109 術語翻譯要保留概念角色</a>：同一條紀律在不同介面 — #109 管跨語言轉述（翻譯時概念在原文的角色不能換位）、本卡管跨卡片轉述（引用時概念在被引卡的分類位置不能換位）。兩者的失誤機制相同：轉述者用自己的理解重新組織了對方的結構。</li>
<li><a href="/blog/report/terminology-keeps-original-anchor/" data-link-title="術語翻譯要保留原文錨點" data-link-desc="翻譯術語時、中文名稱負責降低閱讀門檻，原文名稱負責鎖定概念邊界。只留中文會把 reader 帶進中文詞的日常歧義，只留英文會提高閱讀成本；中文後接英文括號是技術文章的穩定折衷。">#107 術語翻譯要保留原文錨點</a>：#107 的「保留原文讓概念可回溯」在引用場景的對應物 — 引用句裡被引卡的術語就是錨點、錨點寫錯、回溯就斷。</li>
<li><a href="/blog/report/fact-vs-derive-citation-layering/" data-link-title="引用案例要分觀察層 / 判讀層、強化詞是錯位訊號" data-link-desc="引用案例（特別是 rich case）時、case 內容分兩層：觀察層（具體 fact）跟判讀層（作者推論）；兩層在章節引用時要分層標明、避免把作者判讀升級成 case fact；強化詞（才是 / 必須 / 一定 / 關鍵是）通常是錯位訊號、保留 case 原文的條件性表述（取決於 / 核心瓶頸 / 主要驅動）">#116 引用案例要分觀察層 / 判讀層</a>：跟 #116 都在守引用的準確性。#116 管引用內容的層次標註（哪些是 case 的事實、哪些是本文推導）、本卡管引用所用詞彙的歸屬（描述對方時用對方的詞）— 兩者都在防「引用方的加工被讀者當成被引方的原文」。</li>
<li><a href="/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">#97 Metadata surface 要納入寫作 review 範圍</a>：本卡的觸發 case 恰好是引用 #97 時錯置了它的分類 — fact-check reviewer 打開 #97 的分類表逐欄比對才抓到。關係段的 review 方法就是這個動作：逐條開被引卡核對。</li>
</ul>
<h2 id="觸發-case">觸發 case</h2>
<p>一張新卡的關係段寫「引用句是 metadata surface 的一種 — 它不是正文論述、是指向結構的導航層」、引用對象是「Metadata surface 要納入寫作 review 範圍」卡。Fact-check 審查打開被引卡、發現它的分類表把 link label / 索引條目列在 <strong>navigation surface</strong>、跟 metadata surface 是並列的兩列 — 引用句把對方明確分開的兩類併成了一類。判定原文：「關係精神成立（非正文 surface 要納入 review 掃描面）、但嚴格按被引卡的詞彙、引用句更接近 navigation surface」。修法：引用句改成「引用句屬於該卡分類中的 navigation surface（跟 link label、索引條目同層）」— 關係宣告不變、詞彙歸位。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>寫關係段時沒有重新打開被引卡 — 不論多熟、開來把結論段跟分類表掃一遍再寫。</li>
<li>引用句使用了被引卡的專有分類詞（surface 類型、層級名、模式名）— 逐詞回原卡確認歸屬、這些詞是對方的 taxonomy、不是公共詞彙。</li>
<li>review 關係段的方法：每條關係宣告配一次「開被引卡、找到支撐句」的核對、找不到支撐句的宣告就是憑印象寫的。</li>
</ul>
]]></content:encoded></item><item><title>多階段流程的 artifact 欄位契約：下游宣稱的輸入要能從上游產出推導、推導規則要明文</title><link>https://tarrragon.github.io/blog/report/pipeline-artifact-field-contract/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pipeline-artifact-field-contract/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>「Stage B 以 Stage A 的清單為輸入」— 這句話讀起來像契約、實際上常常只是相鄰。它成立的條件在欄位層：&lt;strong>下游輸入的每一欄、要能對到上游產出的某個欄、或一條明文的推導規則&lt;/strong> — B 需要的每欄、A 的表裡有、或文件寫明「B 的欄 y 由 A 的欄 x 依規則 R 推導」。多階段流程（訪談協議、ETL、表單審批、CI pipeline）的每個交接處都適用這條檢查。&lt;/p>
&lt;p>缺口的形態很安靜：A 的表七欄、B 要求的第八種資訊不在七欄裡、也沒有任何地方說明從哪來 — 執行者到了 B 才發現缺、回頭從 A 的欄位自行腦補。可推導時每個執行者推得不一樣、不可推導時就空著或亂填；而缺的欄位往往正是流程分支的開關（「失敗語意 = 不可丟」觸發整個 durable 機制）、開關欄自由心證等於分支隨機。&lt;/p>
&lt;h2 id="能推導跟有規則是兩回事">能推導、跟有規則、是兩回事&lt;/h2>
&lt;p>「語意上推得出來」會讓設計者誤以為不用寫：失敗語意「顯然」能從失敗情境跟風險欄看出來。沒有明文規則時、三個執行者會用三種標準推、而且都覺得自己推得有道理。&lt;/p>
&lt;p>缺口在設計期隱形的原因是接縫沒有 owner：A 的表為 A 的目的設計（盤點操作）、B 的輸入為 B 的目的宣告（標 event 語意）、每一份單獨 review 都通過 — A 的作者覺得交付了、B 的作者覺得宣告了、欄位對不對得上沒有掛在任何人名下。浮現的唯一方法是把 B 的輸入逐欄對 A 的產出走一遍。&lt;/p>
&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>下游欄位靠執行者從上游「看得出來」&lt;/td>
 &lt;td>寫明推導規則：「欄 y：上游欄 x 含金流 / 合約 / 通知義務 → 不可丟」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在上游表直接加欄、服務下游需求&lt;/td>
 &lt;td>可以、但要判斷：欄位屬於上游的語意就加欄、屬於下游的加工就寫推導規則 — 上游表塞滿下游欄位會失去自己的焦點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>交接缺口出現時補一句「參考上游自行判斷」&lt;/td>
 &lt;td>這是把自由心證制度化、不是修復&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只修這一個欄、不掃其他接縫&lt;/td>
 &lt;td>一個缺口浮現時、對全部階段交接跑一次逐欄走查 — 缺口成因（分開設計）作用在每個接縫上&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>逐欄走查的操作：列出下游輸入格式的欄位清單、每欄標注來源 —「上游欄位直給」「明文推導規則」「缺」三種狀態、「缺」逐個補。同一條契約在機器管線同樣成立：CI 的 deploy stage 宣稱以 build artifact 為輸入、需要的 version tag 卻沒有任何 stage 產 — 差別只在機器管線會當場報錯、文件協議的缺口靜默到執行者卡住那天。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/review-miss-diagnose-design-vs-execution-gap/" data-link-title="Review 漏抓先分 design gap 與 execution gap、再決定改框架還是改執行" data-link-desc="Review 漏抓某類問題時，有兩個不同成因：design gap（框架根本沒有對應 frame）跟 execution gap（框架有 frame、但 reviewer 沒跑）。修法相反 —— design gap 要改框架（補 frame / keyword）、execution gap 要改執行（真的跑完該跑的輪）。診斷前先分清：把 execution gap 誤判成 design gap 會 framework bloat（一直加 frame 卻沒解決偷跑子集）、把 design gap 誤判成 execution gap 會永遠漏同類。常見陷阱是『加 keyword』感覺像進步、但對沒跑的輪毫無幫助。">#153 Review 漏抓先分 design gap 與 execution gap&lt;/a>：#153 立卡情境是 review 流程的漏抓、這裡把它的診斷分流借到流程設計：執行者標不出失敗語意、不是不認真、是設計沒給推導規則 — design gap、修法在文件、不在叮嚀執行者。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/verification-timeline-checkpoints/" data-link-title="驗收的時間軸：四個 checkpoint" data-link-desc="驗收不是單一動作、是分散在四個時點（寫之前 / 開發中 / ship 前 / ship 後）的累積判斷。每個 checkpoint 能 catch 不同類型的失敗、成本不同。早期 checkpoint 抓越多、晚期 checkpoint 越輕鬆。實務上常常 collapse 成「寫的時候 &amp;#43; ship 後出問題才修」、跳過寫之前 / ship 前。">#68 驗收的時間軸：四個 checkpoint&lt;/a>：階段交接處是天然的驗收 checkpoint — 逐欄走查就是交接處的驗收動作、在「寫之前」的 checkpoint 跑一次、比執行者在第三階段卡住才回頭便宜。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth&lt;/a>：推導規則本身要有單一明文位置 — 沒有明文規則時、規則存在於每個執行者腦中、是 N 份互不一致的隱性源；寫下來不只是溝通、是把規則收斂成單源。&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/decision-table-conflict-reveals-missing-dimension/" data-link-title="決策表兩列同時命中且結論相反：缺的是一個上游區分維度" data-link-desc="判讀表 / 決策表的兩列規則被同一個真實案例同時命中、且指向相反結論時、問題通常出在表外：案例承載著兩種身分、而表缺少把身分拆開的上游維度 — 修法是補前置澄清問、把維度抬到表之前；拆不出身分的矛盾才是規則真衝突、回表內改規則。偵測方法是用真實案例 dry-run、不是逐列檢查 — 單列都正確的表仍可能整體矛盾。">#158 決策表兩列同時命中且結論相反&lt;/a>：同一族的「組合失效」— #158 是規則之間的矛盾、本卡是階段之間的缺口、偵測都要跨單元走查（#158 靠真實案例 dry-run、本卡靠逐欄對照）而不是單元內檢查；兩者常在同一次旅程模擬中被一起抓到。&lt;/li>
&lt;/ul>
&lt;h2 id="觸發-case">觸發 case&lt;/h2>
&lt;p>一個訪談協議的操作盤點階段產出七欄表（操作 / 角色 / 主情境 / 失敗情境 / 風險類型 / 前端引導 / 後端防護）、domain / event 切分階段的 event catalog 要求每個 event 標「失敗語意（可丟 / 不可丟）」— 且這一欄是 async-queue 維度升級為必展開的開關。讀者旅程 reviewer dry-run 時發現：七欄裡沒有失敗語意、兩份 reference 也都沒說明從哪推導、「runtime 要從失敗情境自行推導、可推但容易漏標」。修復落在 event catalog 的產出格式旁、把推導規則寫成明文 —「操作牽涉金流、合約或通知義務 → 它的 event 不可丟；event 只驅動可重算的衍生結果 → 可丟」— 上游的表一欄都沒加。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>「Stage B 以 Stage A 的清單為輸入」— 這句話讀起來像契約、實際上常常只是相鄰。它成立的條件在欄位層：<strong>下游輸入的每一欄、要能對到上游產出的某個欄、或一條明文的推導規則</strong> — B 需要的每欄、A 的表裡有、或文件寫明「B 的欄 y 由 A 的欄 x 依規則 R 推導」。多階段流程（訪談協議、ETL、表單審批、CI pipeline）的每個交接處都適用這條檢查。</p>
<p>缺口的形態很安靜：A 的表七欄、B 要求的第八種資訊不在七欄裡、也沒有任何地方說明從哪來 — 執行者到了 B 才發現缺、回頭從 A 的欄位自行腦補。可推導時每個執行者推得不一樣、不可推導時就空著或亂填；而缺的欄位往往正是流程分支的開關（「失敗語意 = 不可丟」觸發整個 durable 機制）、開關欄自由心證等於分支隨機。</p>
<h2 id="能推導跟有規則是兩回事">能推導、跟有規則、是兩回事</h2>
<p>「語意上推得出來」會讓設計者誤以為不用寫：失敗語意「顯然」能從失敗情境跟風險欄看出來。沒有明文規則時、三個執行者會用三種標準推、而且都覺得自己推得有道理。</p>
<p>缺口在設計期隱形的原因是接縫沒有 owner：A 的表為 A 的目的設計（盤點操作）、B 的輸入為 B 的目的宣告（標 event 語意）、每一份單獨 review 都通過 — A 的作者覺得交付了、B 的作者覺得宣告了、欄位對不對得上沒有掛在任何人名下。浮現的唯一方法是把 B 的輸入逐欄對 A 的產出走一遍。</p>
<h2 id="反模式與修法">反模式與修法</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>下游欄位靠執行者從上游「看得出來」</td>
          <td>寫明推導規則：「欄 y：上游欄 x 含金流 / 合約 / 通知義務 → 不可丟」</td>
      </tr>
      <tr>
          <td>在上游表直接加欄、服務下游需求</td>
          <td>可以、但要判斷：欄位屬於上游的語意就加欄、屬於下游的加工就寫推導規則 — 上游表塞滿下游欄位會失去自己的焦點</td>
      </tr>
      <tr>
          <td>交接缺口出現時補一句「參考上游自行判斷」</td>
          <td>這是把自由心證制度化、不是修復</td>
      </tr>
      <tr>
          <td>只修這一個欄、不掃其他接縫</td>
          <td>一個缺口浮現時、對全部階段交接跑一次逐欄走查 — 缺口成因（分開設計）作用在每個接縫上</td>
      </tr>
  </tbody>
</table>
<p>逐欄走查的操作：列出下游輸入格式的欄位清單、每欄標注來源 —「上游欄位直給」「明文推導規則」「缺」三種狀態、「缺」逐個補。同一條契約在機器管線同樣成立：CI 的 deploy stage 宣稱以 build artifact 為輸入、需要的 version tag 卻沒有任何 stage 產 — 差別只在機器管線會當場報錯、文件協議的缺口靜默到執行者卡住那天。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><a href="/blog/report/review-miss-diagnose-design-vs-execution-gap/" data-link-title="Review 漏抓先分 design gap 與 execution gap、再決定改框架還是改執行" data-link-desc="Review 漏抓某類問題時，有兩個不同成因：design gap（框架根本沒有對應 frame）跟 execution gap（框架有 frame、但 reviewer 沒跑）。修法相反 —— design gap 要改框架（補 frame / keyword）、execution gap 要改執行（真的跑完該跑的輪）。診斷前先分清：把 execution gap 誤判成 design gap 會 framework bloat（一直加 frame 卻沒解決偷跑子集）、把 design gap 誤判成 execution gap 會永遠漏同類。常見陷阱是『加 keyword』感覺像進步、但對沒跑的輪毫無幫助。">#153 Review 漏抓先分 design gap 與 execution gap</a>：#153 立卡情境是 review 流程的漏抓、這裡把它的診斷分流借到流程設計：執行者標不出失敗語意、不是不認真、是設計沒給推導規則 — design gap、修法在文件、不在叮嚀執行者。</li>
<li><a href="/blog/report/verification-timeline-checkpoints/" data-link-title="驗收的時間軸：四個 checkpoint" data-link-desc="驗收不是單一動作、是分散在四個時點（寫之前 / 開發中 / ship 前 / ship 後）的累積判斷。每個 checkpoint 能 catch 不同類型的失敗、成本不同。早期 checkpoint 抓越多、晚期 checkpoint 越輕鬆。實務上常常 collapse 成「寫的時候 &#43; ship 後出問題才修」、跳過寫之前 / ship 前。">#68 驗收的時間軸：四個 checkpoint</a>：階段交接處是天然的驗收 checkpoint — 逐欄走查就是交接處的驗收動作、在「寫之前」的 checkpoint 跑一次、比執行者在第三階段卡住才回頭便宜。</li>
<li><a href="/blog/report/single-source-of-truth/" data-link-title="Single Source of Truth：值的住址只能有一處" data-link-desc="同一個值（CSS token、視覺基準、runtime 量測）的權威來源只能有一個位置 — 多源時會分歧、會漏改、會讓讀者不知道哪個生效。本文是 #3 / #26 / #27 三篇實作的共同抽象。">#44 Single Source of Truth</a>：推導規則本身要有單一明文位置 — 沒有明文規則時、規則存在於每個執行者腦中、是 N 份互不一致的隱性源；寫下來不只是溝通、是把規則收斂成單源。</li>
<li><a href="/blog/report/decision-table-conflict-reveals-missing-dimension/" data-link-title="決策表兩列同時命中且結論相反：缺的是一個上游區分維度" data-link-desc="判讀表 / 決策表的兩列規則被同一個真實案例同時命中、且指向相反結論時、問題通常出在表外：案例承載著兩種身分、而表缺少把身分拆開的上游維度 — 修法是補前置澄清問、把維度抬到表之前；拆不出身分的矛盾才是規則真衝突、回表內改規則。偵測方法是用真實案例 dry-run、不是逐列檢查 — 單列都正確的表仍可能整體矛盾。">#158 決策表兩列同時命中且結論相反</a>：同一族的「組合失效」— #158 是規則之間的矛盾、本卡是階段之間的缺口、偵測都要跨單元走查（#158 靠真實案例 dry-run、本卡靠逐欄對照）而不是單元內檢查；兩者常在同一次旅程模擬中被一起抓到。</li>
</ul>
<h2 id="觸發-case">觸發 case</h2>
<p>一個訪談協議的操作盤點階段產出七欄表（操作 / 角色 / 主情境 / 失敗情境 / 風險類型 / 前端引導 / 後端防護）、domain / event 切分階段的 event catalog 要求每個 event 標「失敗語意（可丟 / 不可丟）」— 且這一欄是 async-queue 維度升級為必展開的開關。讀者旅程 reviewer dry-run 時發現：七欄裡沒有失敗語意、兩份 reference 也都沒說明從哪推導、「runtime 要從失敗情境自行推導、可推但容易漏標」。修復落在 event catalog 的產出格式旁、把推導規則寫成明文 —「操作牽涉金流、合約或通知義務 → 它的 event 不可丟；event 只驅動可重算的衍生結果 → 可丟」— 上游的表一欄都沒加。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<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>
]]></content:encoded></item><item><title>知識載體責任分配：rules、agents、skills 各該裝什麼</title><link>https://tarrragon.github.io/blog/report/knowledge-carrier-responsibility-allocation/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/knowledge-carrier-responsibility-allocation/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>知識寫入框架前，依「&lt;strong>受眾 x 形態&lt;/strong>」二軸決定載體，而不是順手寫在當下開啟中的檔案。核心判定一句話：&lt;strong>代理人定義回答「你是誰、你能做什麼、你偏好怎麼做」；skill 回答「這件事怎麼做」&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>CLAUDE.md&lt;/td>
 &lt;td>所有角色&lt;/td>
 &lt;td>每回合自動&lt;/td>
 &lt;td>專案身份、指令、專案級技術選型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rules/core/&lt;/td>
 &lt;td>所有角色&lt;/td>
 &lt;td>每回合自動&lt;/td>
 &lt;td>行為禁令速查 + 路由（有 token 預算）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>pm-rules/&lt;/td>
 &lt;td>僅主線程 PM（只調度不執行的主對話）&lt;/td>
 &lt;td>情境按需&lt;/td>
 &lt;td>調度流程 SOP（派發、驗收、決策樹）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AGENT_PRELOAD&lt;/td>
 &lt;td>全體代理人&lt;/td>
 &lt;td>派發時注入&lt;/td>
 &lt;td>代理人通用行為禁令&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>agents/*.md&lt;/td>
 &lt;td>單一代理人&lt;/td>
 &lt;td>派發時載入&lt;/td>
 &lt;td>身份、責任邊界、設計偏好（命名、技術手法、語氣）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>skills/&lt;/td>
 &lt;td>觸發者（角色無關）&lt;/td>
 &lt;td>觸發時漸進揭露（先載觸發入口、細節按需展開）&lt;/td>
 &lt;td>可重複工作流與方法（TDD、寫作、ticket CLI）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>methodologies/&lt;/td>
 &lt;td>主動查閱者&lt;/td>
 &lt;td>按需&lt;/td>
 &lt;td>30 秒理念複習清單&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>references/&lt;/td>
 &lt;td>執行特定動作者&lt;/td>
 &lt;td>按需&lt;/td>
 &lt;td>技術參考、規則的完整論證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error-patterns/&lt;/td>
 &lt;td>任務前查詢者&lt;/td>
 &lt;td>按需&lt;/td>
 &lt;td>失敗案例（症狀 / 根因 / 解法 / 預防）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>memory（專案層）&lt;/td>
 &lt;td>僅本專案&lt;/td>
 &lt;td>每回合自動&lt;/td>
 &lt;td>專案特定活教訓的單行索引（教訓升級為正式規則後即移出）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>框架以「主線程只調度、任務交代理人」為分工原則運作。規範自然長出多個放置點：rules 收所有角色通用規則、pm-rules 收主線程專用流程、AGENT_PRELOAD 收代理人通用禁令、各代理人定義檔收身份與偏好、skills 承載工作流、methodologies 承載理念。&lt;/p>
&lt;p>問題在每次學到新東西的時候浮現：&lt;strong>這段知識該寫進哪裡？&lt;/strong> 沒有頂層地圖時，答案是「寫在最不會忘記的地方」，也就是自動載入層。專案演進至今，自動載入集合膨脹到 82.5k tokens（200k context 模型的約 40%），而代理人定義檔裡混進了操作流程步驟、與通用品質規範重複的檢查清單、錯誤案例全文、工具使用指南。這四種都不屬於「這個代理人是誰」的內容。&lt;/p>
&lt;h2 id="演進歷程">演進歷程&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>單檔期&lt;/strong>：一切塞 CLAUDE.md。專案設定、品質規則、流程說明混在一起，單檔暴漲。&lt;/li>
&lt;li>&lt;strong>規則分層期&lt;/strong>：拆出 &lt;code>rules/core/&lt;/code>（通用品質底線）與 &lt;code>pm-rules/&lt;/code>（主線程專用）。動機直接來自分工原則：PM 不寫程式只調度，調度 SOP 沒理由佔用代理人的 context。&lt;/li>
&lt;li>&lt;strong>代理人標準期&lt;/strong>：建立 AGENT_PRELOAD（通用行為禁令，派發時注入）與代理人定義三區塊標準（允許產出 / 禁止行為 / 適用情境），讓派發前可查表確認職責邊界。&lt;/li>
&lt;li>&lt;strong>Skill 承載期&lt;/strong>：可重複的工作流（TDD 流程、寫作方法、ticket 操作）移入 skills，藉觸發時漸進揭露控制 context 成本。&lt;/li>
&lt;li>&lt;strong>Token 收斂期&lt;/strong>：自動載入集合 82.5k 收斂到 41.9k。關鍵發現是膨脹根因不是「規則太多」而是「表達形態錯置」：論證、案例、教學混進了每回合載入的層。建立 45k 預算的機器量測 + 「自動載入層只放禁令與路由」的形態約束。&lt;/li>
&lt;li>&lt;strong>責任地圖期（本篇）&lt;/strong>：收斂解決了「放多少」，本階段補「放哪裡」的頂層判定：受眾 x 形態二軸地圖，並首次權威化「代理人定義該裝什麼」。&lt;/li>
&lt;/ol>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>標題點名的三個載體在此各有答案：rules 的答案在「二軸定位」（自動載入層的形態約束）、agents 與 skills 的答案在「人格與流程的分界」。&lt;/p>
&lt;h3 id="二軸定位先問受眾再問形態">二軸定位，先問受眾再問形態&lt;/h3>
&lt;p>受眾軸縮小候選載體（所有角色 / 僅 PM / 全體代理人 / 單一代理人 / 動作觸發者 / 僅本專案），形態軸確定最終位置（行為禁令 / 調度流程 / 身份偏好 / 工作流方法 / 理念清單 / 技術參考 / 失敗案例 / 專案設定）。兩軸都過了，再檢查目的地是否屬自動載入層並過預算閘門：規範類知識的閘門是必要性否決（「每回合都需要嗎？」）加上把形態壓成「禁令 + 路由」；專案設定這類事實陳述不適用必要性否決，閘門只管體積精簡與不混入框架通用知識。&lt;/p>
&lt;h3 id="代理人定義-vs-skill人格與流程的分界">代理人定義 vs skill：人格與流程的分界&lt;/h3>
&lt;p>代理人檔案該裝的是人格與授權：身份定位、責任邊界三區塊、設計偏好（命名習慣、技術手法傾向、文法語氣）、分工路由。操作流程屬於 skill。流程與人格解耦後，同一個寫作 skill 可以被任何代理人觸發，行為一致；而「該由誰執行、用什麼語氣與偏好執行」才是代理人檔案的事。&lt;/p>
&lt;p>一個容易踩的細節：「技術選型」分兩層。專案級選型（用什麼框架、什麼版本）屬 CLAUDE.md 的專案設定；代理人層放的是「手法傾向」：代理人帶著多種方案的知識，依專案設定選用。把專案選型寫進代理人定義，跨專案 sync 時就會把錯的選型帶去別的專案。&lt;/p>
&lt;h3 id="重複內容一律路由化">重複內容一律路由化&lt;/h3>
&lt;p>品質清單、錯誤案例、工具指南在代理人檔案裡只放一行路由（「品質標準見 quality-common『常數管理』節」「詳見 IMP-003」，錨點用語意標題不用位置編號）。複製全文的代價是漂移：來源更新後副本不會跟著動，兩份規範開始打架。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Token 污染&lt;/strong>：知識預設寫進自動載入層，每 session 固定燒掉 context 的 40%，而且 attention 稀釋會降低規則遵循率：載越多，守得越差。&lt;/li>
&lt;li>&lt;strong>知識失傳&lt;/strong>：跨專案原則寫進專案 memory，不會隨框架 sync，其他專案重踩同樣的雷。&lt;/li>
&lt;li>&lt;strong>規範打架，執法強的一方贏&lt;/strong>：載體錯置會製造規則矛盾（例：寫作規範要求每段完整論證 vs 自動載入層要求壓縮形態）。矛盾規則競爭時勝負不看道理，看執法強度：有 hook 阻擋或常駐審查（每次變更必跑的審查代理人）的一方必勝，只有事後量測的一方形同虛設。這是本階段演進最重要的單一洞察：&lt;strong>載體地圖必須與執法機制對齊，否則正確的原則會輸給有 hook 的舊習慣&lt;/strong>。&lt;/li>
&lt;li>&lt;strong>代理人檔案變成第二份過時手冊&lt;/strong>：流程寫兩份（agent + skill），更新只改一份，派發時代理人讀到舊流程。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&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>寫新知識時的第一反應是「加進 CLAUDE.md / rules 才不會忘」&lt;/td>
 &lt;td>用載入頻率換安全感；該問的是受眾與形態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>代理人定義出現「執行步驟 1-2-3」&lt;/td>
 &lt;td>流程混進人格層，外移 skill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同一份清單出現在兩個檔案&lt;/td>
 &lt;td>漂移倒數計時開始，改路由化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自動載入預算量測逼近上限，而最近沒有新增「每回合行為禁令」&lt;/td>
 &lt;td>有論證或案例混進自動載入層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩條規則對同一動作給出相反指示&lt;/td>
 &lt;td>立刻盤點概念詞重疊的既有規範，矛盾修補與新規則同批落地&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="後續">後續&lt;/h2>
&lt;p>框架內的權威版（30 秒地圖 + 檢查清單）維護在框架 &lt;code>methodologies/knowledge-carrier-allocation-methodology.md&lt;/code>，隨理念演進持續更新；本篇是此階段思考歷程的快照。存量錯置的盤點（30 份代理人定義、57 份方法論的體量稽核）已建 ticket 排程，依地圖逐檔分類後再搬移。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>知識寫入框架前，依「<strong>受眾 x 形態</strong>」二軸決定載體，而不是順手寫在當下開啟中的檔案。核心判定一句話：<strong>代理人定義回答「你是誰、你能做什麼、你偏好怎麼做」；skill 回答「這件事怎麼做」</strong>——前者是人格與授權（換一個代理人就不同），後者是可重複流程（任何角色觸發都應得到同一流程）。</p>
<table>
  <thead>
      <tr>
          <th>載體</th>
          <th>受眾</th>
          <th>載入時機</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CLAUDE.md</td>
          <td>所有角色</td>
          <td>每回合自動</td>
          <td>專案身份、指令、專案級技術選型</td>
      </tr>
      <tr>
          <td>rules/core/</td>
          <td>所有角色</td>
          <td>每回合自動</td>
          <td>行為禁令速查 + 路由（有 token 預算）</td>
      </tr>
      <tr>
          <td>pm-rules/</td>
          <td>僅主線程 PM（只調度不執行的主對話）</td>
          <td>情境按需</td>
          <td>調度流程 SOP（派發、驗收、決策樹）</td>
      </tr>
      <tr>
          <td>AGENT_PRELOAD</td>
          <td>全體代理人</td>
          <td>派發時注入</td>
          <td>代理人通用行為禁令</td>
      </tr>
      <tr>
          <td>agents/*.md</td>
          <td>單一代理人</td>
          <td>派發時載入</td>
          <td>身份、責任邊界、設計偏好（命名、技術手法、語氣）</td>
      </tr>
      <tr>
          <td>skills/</td>
          <td>觸發者（角色無關）</td>
          <td>觸發時漸進揭露（先載觸發入口、細節按需展開）</td>
          <td>可重複工作流與方法（TDD、寫作、ticket CLI）</td>
      </tr>
      <tr>
          <td>methodologies/</td>
          <td>主動查閱者</td>
          <td>按需</td>
          <td>30 秒理念複習清單</td>
      </tr>
      <tr>
          <td>references/</td>
          <td>執行特定動作者</td>
          <td>按需</td>
          <td>技術參考、規則的完整論證</td>
      </tr>
      <tr>
          <td>error-patterns/</td>
          <td>任務前查詢者</td>
          <td>按需</td>
          <td>失敗案例（症狀 / 根因 / 解法 / 預防）</td>
      </tr>
      <tr>
          <td>memory（專案層）</td>
          <td>僅本專案</td>
          <td>每回合自動</td>
          <td>專案特定活教訓的單行索引（教訓升級為正式規則後即移出）</td>
      </tr>
  </tbody>
</table>
<h2 id="情境">情境</h2>
<p>框架以「主線程只調度、任務交代理人」為分工原則運作。規範自然長出多個放置點：rules 收所有角色通用規則、pm-rules 收主線程專用流程、AGENT_PRELOAD 收代理人通用禁令、各代理人定義檔收身份與偏好、skills 承載工作流、methodologies 承載理念。</p>
<p>問題在每次學到新東西的時候浮現：<strong>這段知識該寫進哪裡？</strong> 沒有頂層地圖時，答案是「寫在最不會忘記的地方」，也就是自動載入層。專案演進至今，自動載入集合膨脹到 82.5k tokens（200k context 模型的約 40%），而代理人定義檔裡混進了操作流程步驟、與通用品質規範重複的檢查清單、錯誤案例全文、工具使用指南。這四種都不屬於「這個代理人是誰」的內容。</p>
<h2 id="演進歷程">演進歷程</h2>
<ol>
<li><strong>單檔期</strong>：一切塞 CLAUDE.md。專案設定、品質規則、流程說明混在一起，單檔暴漲。</li>
<li><strong>規則分層期</strong>：拆出 <code>rules/core/</code>（通用品質底線）與 <code>pm-rules/</code>（主線程專用）。動機直接來自分工原則：PM 不寫程式只調度，調度 SOP 沒理由佔用代理人的 context。</li>
<li><strong>代理人標準期</strong>：建立 AGENT_PRELOAD（通用行為禁令，派發時注入）與代理人定義三區塊標準（允許產出 / 禁止行為 / 適用情境），讓派發前可查表確認職責邊界。</li>
<li><strong>Skill 承載期</strong>：可重複的工作流（TDD 流程、寫作方法、ticket 操作）移入 skills，藉觸發時漸進揭露控制 context 成本。</li>
<li><strong>Token 收斂期</strong>：自動載入集合 82.5k 收斂到 41.9k。關鍵發現是膨脹根因不是「規則太多」而是「表達形態錯置」：論證、案例、教學混進了每回合載入的層。建立 45k 預算的機器量測 + 「自動載入層只放禁令與路由」的形態約束。</li>
<li><strong>責任地圖期（本篇）</strong>：收斂解決了「放多少」，本階段補「放哪裡」的頂層判定：受眾 x 形態二軸地圖，並首次權威化「代理人定義該裝什麼」。</li>
</ol>
<h2 id="理想做法">理想做法</h2>
<p>標題點名的三個載體在此各有答案：rules 的答案在「二軸定位」（自動載入層的形態約束）、agents 與 skills 的答案在「人格與流程的分界」。</p>
<h3 id="二軸定位先問受眾再問形態">二軸定位，先問受眾再問形態</h3>
<p>受眾軸縮小候選載體（所有角色 / 僅 PM / 全體代理人 / 單一代理人 / 動作觸發者 / 僅本專案），形態軸確定最終位置（行為禁令 / 調度流程 / 身份偏好 / 工作流方法 / 理念清單 / 技術參考 / 失敗案例 / 專案設定）。兩軸都過了，再檢查目的地是否屬自動載入層並過預算閘門：規範類知識的閘門是必要性否決（「每回合都需要嗎？」）加上把形態壓成「禁令 + 路由」；專案設定這類事實陳述不適用必要性否決，閘門只管體積精簡與不混入框架通用知識。</p>
<h3 id="代理人定義-vs-skill人格與流程的分界">代理人定義 vs skill：人格與流程的分界</h3>
<p>代理人檔案該裝的是人格與授權：身份定位、責任邊界三區塊、設計偏好（命名習慣、技術手法傾向、文法語氣）、分工路由。操作流程屬於 skill。流程與人格解耦後，同一個寫作 skill 可以被任何代理人觸發，行為一致；而「該由誰執行、用什麼語氣與偏好執行」才是代理人檔案的事。</p>
<p>一個容易踩的細節：「技術選型」分兩層。專案級選型（用什麼框架、什麼版本）屬 CLAUDE.md 的專案設定；代理人層放的是「手法傾向」：代理人帶著多種方案的知識，依專案設定選用。把專案選型寫進代理人定義，跨專案 sync 時就會把錯的選型帶去別的專案。</p>
<h3 id="重複內容一律路由化">重複內容一律路由化</h3>
<p>品質清單、錯誤案例、工具指南在代理人檔案裡只放一行路由（「品質標準見 quality-common『常數管理』節」「詳見 IMP-003」，錨點用語意標題不用位置編號）。複製全文的代價是漂移：來源更新後副本不會跟著動，兩份規範開始打架。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>Token 污染</strong>：知識預設寫進自動載入層，每 session 固定燒掉 context 的 40%，而且 attention 稀釋會降低規則遵循率：載越多，守得越差。</li>
<li><strong>知識失傳</strong>：跨專案原則寫進專案 memory，不會隨框架 sync，其他專案重踩同樣的雷。</li>
<li><strong>規範打架，執法強的一方贏</strong>：載體錯置會製造規則矛盾（例：寫作規範要求每段完整論證 vs 自動載入層要求壓縮形態）。矛盾規則競爭時勝負不看道理，看執法強度：有 hook 阻擋或常駐審查（每次變更必跑的審查代理人）的一方必勝，只有事後量測的一方形同虛設。這是本階段演進最重要的單一洞察：<strong>載體地圖必須與執法機制對齊，否則正確的原則會輸給有 hook 的舊習慣</strong>。</li>
<li><strong>代理人檔案變成第二份過時手冊</strong>：流程寫兩份（agent + skill），更新只改一份，派發時代理人讀到舊流程。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>下次遇到這些訊號，代表載體錯置正在發生：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>診斷與行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫新知識時的第一反應是「加進 CLAUDE.md / rules 才不會忘」</td>
          <td>用載入頻率換安全感；該問的是受眾與形態</td>
      </tr>
      <tr>
          <td>代理人定義出現「執行步驟 1-2-3」</td>
          <td>流程混進人格層，外移 skill</td>
      </tr>
      <tr>
          <td>同一份清單出現在兩個檔案</td>
          <td>漂移倒數計時開始，改路由化</td>
      </tr>
      <tr>
          <td>自動載入預算量測逼近上限，而最近沒有新增「每回合行為禁令」</td>
          <td>有論證或案例混進自動載入層</td>
      </tr>
      <tr>
          <td>兩條規則對同一動作給出相反指示</td>
          <td>立刻盤點概念詞重疊的既有規範，矛盾修補與新規則同批落地</td>
      </tr>
  </tbody>
</table>
<h2 id="後續">後續</h2>
<p>框架內的權威版（30 秒地圖 + 檢查清單）維護在框架 <code>methodologies/knowledge-carrier-allocation-methodology.md</code>，隨理念演進持續更新；本篇是此階段思考歷程的快照。存量錯置的盤點（30 份代理人定義、57 份方法論的體量稽核）已建 ticket 排程，依地圖逐檔分類後再搬移。</p>
]]></content:encoded></item><item><title>register 違規：偵測可機械化、判定要靠文體異源的眼睛</title><link>https://tarrragon.github.io/blog/report/register-violation-needs-cross-style-eyes/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/register-violation-needs-cross-style-eyes/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫作規範的違規分兩類、判定性質根本不同。形式違規（emoji、編號、broken link、裸 URL、frontmatter 缺欄）可完全機械判定、命中即違規、該進工具鏈。register / 品味違規（核心概念前置、否定起手、喊話、誇飾、口語修辭）的判定有不可消除的品味核心、偵測可機械化但判定不可。&lt;/p>
&lt;p>「不是 X、而是 Y」是 register 違規裡最隱蔽的一種、因為它的偵測可機械化（句型明顯、grep 抓得到）、這個「偵測可機械化」偽裝成「判定也可機械化」。被這個偽裝誤導、就會無限投入更精緻的判定方法（補 grep → 概念位置判準 → 行為測試）、而判定（這個否定在建立概念還是做對照）本質是品味判斷、始終在品味側、始終放水。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>後續校正（見 &lt;a href="../lead-with-the-point-cross-language/">#166 重點優先陳述是跨語言的資訊結構原則&lt;/a>）&lt;/strong>：「不是 X、而是 Y」這個子集後來被釐清為資訊結構問題（重點後置）、不是純品味 —— 判定其實可操作（核心概念第一次正面出現在不在句首）、主解是強制執行重點位置判準、異源視角降為補充。本卡論的「品味核心 / 同源上限 / 異源為真防線」對喊話、誇飾這類無單一重點位置的 register 仍成立、但對「重點優先」這個可操作子集是過度推論。&lt;/p>&lt;/blockquote>
&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>emoji、編號、broken link、frontmatter&lt;/td>
 &lt;td>確定性、命中即違規&lt;/td>
 &lt;td>工具鏈 lint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>register 違規&lt;/td>
 &lt;td>概念前置、否定起手、喊話、誇飾、口語&lt;/td>
 &lt;td>有不可消除的品味核心&lt;/td>
 &lt;td>文體異源視角 + 工具鏈曝光&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="為什麼-register-違規的判定不可機械化">為什麼 register 違規的判定不可機械化&lt;/h2>
&lt;p>判定要回答「這個句子讀起來對不對」、這是品味問題、品味需要一個會讀的主體去感受、沒有可機械化的判準能取代。「不是 X、而是 Y」的判定要區分兩種角色：&lt;/p>
&lt;ul>
&lt;li>建立核心概念的否定（違規）：「外包一塊能力&lt;strong>不是&lt;/strong>二元、&lt;strong>而是&lt;/strong>有深度的」—— 核心概念「有深度」被擠到「而是」之後、句首先丟一個被否定的錯誤理解。&lt;/li>
&lt;li>反例對照的否定（合規）：在明示反例段落裡、否定就是對照本體（見 &lt;a href="../positive-rewrite-preserves-contrast/">#94&lt;/a>）。&lt;/li>
&lt;/ul>
&lt;p>兩者 grep 命中長得一樣、語意角色相反。要判對、reviewer 得讀懂段落結構、感受核心概念的位置 —— 這是品味判斷。任何想把它機械化的嘗試都只是換個地方做品味判斷：「概念位置判準」要判斷哪句是概念句、「刪除測試」要判斷剩下的概念完不完整、終點都回到「這讀起來對不對」。偵測的可機械化掩蓋了這個事實。&lt;/p>
&lt;hr>
&lt;h2 id="同源自審的結構上限">同源自審的結構上限&lt;/h2>
&lt;p>更深一層的原因、解釋了為什麼再多輪 LLM 審查都跨不過：產出 register 違規的主體跟審查它的主體共享同一套文體直覺。「不是 X、而是 Y」是 LLM 高頻自產的定義句型、LLM reviewer 讀到它「讀起來自然、權威」、違規感不觸發 —— 作者的盲區跟 reviewer 的盲區是同一個。&lt;/p>
&lt;p>這讓 register 違規的同源自審有結構上限。加 reviewer 數量、加審查輪次、換 frame、都在同一套文體直覺內打轉、跨不過盲區。要看出 register 違規、需要一雙「不共享作者文體」的眼睛。&lt;/p>
&lt;p>這條上限是 &lt;a href="../keyword-bank-hit-is-candidate-not-verdict/">#149&lt;/a>（命中後判定會放水）跟 &lt;a href="../rule-codification-vs-self-audit/">#147&lt;/a>（立規範不等於自審執行）的共同根因：#147 的修法是「立規範當下轉 grep + sweep」、#149 的修法是「命中後用概念位置判定」、兩個修法都還在自動化側、對 register 違規都攔不住、因為判定的品味核心要異源的眼睛才感受得到。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>把違規先分類、再配對應的防線。&lt;/p>
&lt;h3 id="第一步分類--這個違規在哪一側">第一步：分類 —— 這個違規在哪一側&lt;/h3>
&lt;p>問一個判別問題：「這條規則的判定、是命中即違規（確定性）、還是要讀懂句子讀起來對不對（品味）？」確定性的進工具鏈、品味的進異源審查。emoji、編號、broken link 在確定性側；概念前置、否定起手、喊話、誇飾在品味側。&lt;/p>
&lt;h3 id="第二步register-違規--偵測機械化判定靠異源">第二步：register 違規 —— 偵測機械化、判定靠異源&lt;/h3>
&lt;p>偵測仍用 grep、但定位是「曝光候選給異源視角抽查」、產出不是「自動判定違規」。判定交給文體異源的眼睛：&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>作者以外的人讀、register 違規對非作者最刺眼（本卡就是這樣抓到的）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reader-simulation&lt;/td>
 &lt;td>刻意換一個讀者視角讀每句「這句給新資訊、還是文體慣性」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對抗文體 reviewer&lt;/td>
 &lt;td>prompt 明確要求 reviewer 採「挑剔否定起手 / 概念後置」的對抗姿態、抵銷同源文體慣性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="第三步接受-100-自動-catch-不可能">第三步：接受 100% 自動 catch 不可能&lt;/h3>
&lt;p>把 register 違規的審查目標設成「曝光候選 + 異源抽查」、而不是「自動判定 clean」。回報時誠實標「register 層靠異源抽查、未做窮舉自動判定」、不把同源自審的 clean 當成真 clean。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="在錯的一側無限用力">在錯的一側無限用力&lt;/h3>
&lt;p>把 register 違規當成自動化側問題、就會持續投入「更好的檢查方法」（補 grep、寫判準、做測試）、每次都改進一點、每次都還是放水 —— 因為判定始終在品味側。投入的努力跟結果脫節、看起來在進步、實際在原地。&lt;/p>
&lt;h3 id="同源-clean-製造虛假信心">同源 clean 製造虛假信心&lt;/h3>
&lt;p>跑完多輪 LLM 審查、回報「字句層 clean」、這個 clean 是同源盲區的產物、不是真 clean。比沒審更危險：沒審還知道沒查、同源審完誤以為查過了、register 違規帶著「已審查」標籤留在稿件裡。&lt;/p>
&lt;h3 id="規範卡越多越以為夠了">規範卡越多、越以為夠了&lt;/h3>
&lt;p>blog 累積了 #94 / #147 / #149 一整套 register 審查原則、看起來防護很完整、反而讓人以為「照卡片做就夠」。這次的失敗證明：原則齊備、修法齊備、同源執行仍跨不過上限。原則的數量不等於防線的強度。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 字句層 review：keyword bank 命中是候選、不是判決&lt;/a>&lt;/strong>：#149 揭露「命中後判定會放水」這個現象、本卡是它的上層 —— 揭露放水的根因（判定在品味側、同源自審有結構上限）跟結構解（異源視角、不是更好的判定準則）。#149 的「概念位置判準」是好的判準、但它仍要同源 reviewer 執行品味判斷、本卡指出這一步有上限。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../rule-codification-vs-self-audit/">#147 規範化跟自審是兩種認知任務&lt;/a>&lt;/strong>：#147 講「立規範不等於自審執行」、修法是轉 grep + sweep。本卡補它沒覆蓋的：對 register 違規、grep + sweep 命中了、判定仍跨不過同源盲區。#147 解「自審有沒有做」、本卡解「自審做了、為什麼對 register 仍失效」。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精修&lt;/a>&lt;/strong>：本卡把 #82 的「字面 vs 行為」推到極限 —— 對 register 違規、連「行為精修」（讀者能不能驗證）都還是品味判斷、需要異源的讀者才感受得到。形式違規停在字面層即可、register 違規連行為層都需要異源。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review scope 要蓋同類風險區&lt;/a>&lt;/strong>：#95 解 scope 軸（同類違規要 corpus-wide grep）、本卡解 source 軸（同類違規的判定要文體異源）。兩個一起才完整：grep 把候選掃出來（#95）、異源的眼睛判它違不違規（本卡）。這次三個 surface 鏡像內容共享同一違規句型、grep 掃得到、但同源判定全放行 —— scope 對了、source 沒換。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點&lt;/a>&lt;/strong>：#114 解偵測層（展開成 keyword bank）、本卡指出對 register 違規、偵測層做好了、判定層仍需異源 —— frame 顆粒度再細、同源 reviewer 對品味違規仍有盲區。&lt;/li>
&lt;/ul>
&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>對某個寫作違規、一直在「換更好的檢查方法」（補 grep / 寫判準 / 做測試）&lt;/td>
 &lt;td>停 —— 先問它在品味側還形式側、品味側的解是換 source、不是換 method&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個違規「偵測明顯（grep 抓得到）」但「判定要讀懂語意」&lt;/td>
 &lt;td>偵測可機械化、判定在品味側、別把偵測的容易誤當判定的容易&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多輪 LLM 審查全 clean、但作者以外的人一眼看到違規&lt;/td>
 &lt;td>同源盲區的 retro signal、這類違規需要文體異源視角&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>reviewer 全是 LLM、prompt 全是「自然語言判準」&lt;/td>
 &lt;td>register 層缺異源防線、補人類冷讀或對抗文體 reviewer&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回報「字句層 clean」、但只跑了同源 reviewer&lt;/td>
 &lt;td>這個 clean 可能是同源盲區產物、標「register 層未經異源抽查」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：register / 品味類寫作規範（正向陳述、概念前置、喊話、誇飾、口語修辭、文體）的 review 流程設計；AI 輔助寫作的 self-review（最容易在 register 層同源放水）；multi-round-review 的 frame 與資源配置。&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：形式違規（emoji、編號、broken link、frontmatter）—— 這類確定性判定、進工具鏈即可、不需異源、本卡的「同源上限」不適用。&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：「同源自審有上限」不等於「同源自審無用」 —— 同源 reviewer 仍能抓形式違規、抓部分明顯的 register 違規；上限指的是「對 register 違規無法做到窮舉可靠」、所以要補異源、不是廢掉同源。也不等於「只能靠人」 —— 偵測機械化、對抗文體 reviewer、reader-simulation 都能降低對純人工的依賴、只是接受殘餘要異源抽查。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源&lt;/h2>
&lt;p>本卡觸發於一次 backend 章節 + skill 的四輪 multi-round-review。流程是 12 個 LLM reviewer、4 輪、frame 涵蓋規範遵循 / 技術 / 一致性 / cadence / reader-simulation / title / self-application / steelman / outbound / cold-read / misuse / regression。Round 1-A 與 Round 3-A 都跑了 &lt;code>compositional-writing&lt;/code> 的正向陳述 grep keyword bank、命中了三個 surface 的「不是 X、而是 Y」、全部判成「合規對照」放行、回報「字句層 clean」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫作規範的違規分兩類、判定性質根本不同。形式違規（emoji、編號、broken link、裸 URL、frontmatter 缺欄）可完全機械判定、命中即違規、該進工具鏈。register / 品味違規（核心概念前置、否定起手、喊話、誇飾、口語修辭）的判定有不可消除的品味核心、偵測可機械化但判定不可。</p>
<p>「不是 X、而是 Y」是 register 違規裡最隱蔽的一種、因為它的偵測可機械化（句型明顯、grep 抓得到）、這個「偵測可機械化」偽裝成「判定也可機械化」。被這個偽裝誤導、就會無限投入更精緻的判定方法（補 grep → 概念位置判準 → 行為測試）、而判定（這個否定在建立概念還是做對照）本質是品味判斷、始終在品味側、始終放水。</p>
<blockquote>
<p><strong>後續校正（見 <a href="../lead-with-the-point-cross-language/">#166 重點優先陳述是跨語言的資訊結構原則</a>）</strong>：「不是 X、而是 Y」這個子集後來被釐清為資訊結構問題（重點後置）、不是純品味 —— 判定其實可操作（核心概念第一次正面出現在不在句首）、主解是強制執行重點位置判準、異源視角降為補充。本卡論的「品味核心 / 同源上限 / 異源為真防線」對喊話、誇飾這類無單一重點位置的 register 仍成立、但對「重點優先」這個可操作子集是過度推論。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>違規類型</th>
          <th>例子</th>
          <th>判定性質</th>
          <th>防線</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>形式違規</td>
          <td>emoji、編號、broken link、frontmatter</td>
          <td>確定性、命中即違規</td>
          <td>工具鏈 lint</td>
      </tr>
      <tr>
          <td>register 違規</td>
          <td>概念前置、否定起手、喊話、誇飾、口語</td>
          <td>有不可消除的品味核心</td>
          <td>文體異源視角 + 工具鏈曝光</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="為什麼-register-違規的判定不可機械化">為什麼 register 違規的判定不可機械化</h2>
<p>判定要回答「這個句子讀起來對不對」、這是品味問題、品味需要一個會讀的主體去感受、沒有可機械化的判準能取代。「不是 X、而是 Y」的判定要區分兩種角色：</p>
<ul>
<li>建立核心概念的否定（違規）：「外包一塊能力<strong>不是</strong>二元、<strong>而是</strong>有深度的」—— 核心概念「有深度」被擠到「而是」之後、句首先丟一個被否定的錯誤理解。</li>
<li>反例對照的否定（合規）：在明示反例段落裡、否定就是對照本體（見 <a href="../positive-rewrite-preserves-contrast/">#94</a>）。</li>
</ul>
<p>兩者 grep 命中長得一樣、語意角色相反。要判對、reviewer 得讀懂段落結構、感受核心概念的位置 —— 這是品味判斷。任何想把它機械化的嘗試都只是換個地方做品味判斷：「概念位置判準」要判斷哪句是概念句、「刪除測試」要判斷剩下的概念完不完整、終點都回到「這讀起來對不對」。偵測的可機械化掩蓋了這個事實。</p>
<hr>
<h2 id="同源自審的結構上限">同源自審的結構上限</h2>
<p>更深一層的原因、解釋了為什麼再多輪 LLM 審查都跨不過：產出 register 違規的主體跟審查它的主體共享同一套文體直覺。「不是 X、而是 Y」是 LLM 高頻自產的定義句型、LLM reviewer 讀到它「讀起來自然、權威」、違規感不觸發 —— 作者的盲區跟 reviewer 的盲區是同一個。</p>
<p>這讓 register 違規的同源自審有結構上限。加 reviewer 數量、加審查輪次、換 frame、都在同一套文體直覺內打轉、跨不過盲區。要看出 register 違規、需要一雙「不共享作者文體」的眼睛。</p>
<p>這條上限是 <a href="../keyword-bank-hit-is-candidate-not-verdict/">#149</a>（命中後判定會放水）跟 <a href="../rule-codification-vs-self-audit/">#147</a>（立規範不等於自審執行）的共同根因：#147 的修法是「立規範當下轉 grep + sweep」、#149 的修法是「命中後用概念位置判定」、兩個修法都還在自動化側、對 register 違規都攔不住、因為判定的品味核心要異源的眼睛才感受得到。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<p>把違規先分類、再配對應的防線。</p>
<h3 id="第一步分類--這個違規在哪一側">第一步：分類 —— 這個違規在哪一側</h3>
<p>問一個判別問題：「這條規則的判定、是命中即違規（確定性）、還是要讀懂句子讀起來對不對（品味）？」確定性的進工具鏈、品味的進異源審查。emoji、編號、broken link 在確定性側；概念前置、否定起手、喊話、誇飾在品味側。</p>
<h3 id="第二步register-違規--偵測機械化判定靠異源">第二步：register 違規 —— 偵測機械化、判定靠異源</h3>
<p>偵測仍用 grep、但定位是「曝光候選給異源視角抽查」、產出不是「自動判定違規」。判定交給文體異源的眼睛：</p>
<table>
  <thead>
      <tr>
          <th>異源來源</th>
          <th>怎麼用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>人類冷讀</td>
          <td>作者以外的人讀、register 違規對非作者最刺眼（本卡就是這樣抓到的）</td>
      </tr>
      <tr>
          <td>reader-simulation</td>
          <td>刻意換一個讀者視角讀每句「這句給新資訊、還是文體慣性」</td>
      </tr>
      <tr>
          <td>對抗文體 reviewer</td>
          <td>prompt 明確要求 reviewer 採「挑剔否定起手 / 概念後置」的對抗姿態、抵銷同源文體慣性</td>
      </tr>
  </tbody>
</table>
<h3 id="第三步接受-100-自動-catch-不可能">第三步：接受 100% 自動 catch 不可能</h3>
<p>把 register 違規的審查目標設成「曝光候選 + 異源抽查」、而不是「自動判定 clean」。回報時誠實標「register 層靠異源抽查、未做窮舉自動判定」、不把同源自審的 clean 當成真 clean。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="在錯的一側無限用力">在錯的一側無限用力</h3>
<p>把 register 違規當成自動化側問題、就會持續投入「更好的檢查方法」（補 grep、寫判準、做測試）、每次都改進一點、每次都還是放水 —— 因為判定始終在品味側。投入的努力跟結果脫節、看起來在進步、實際在原地。</p>
<h3 id="同源-clean-製造虛假信心">同源 clean 製造虛假信心</h3>
<p>跑完多輪 LLM 審查、回報「字句層 clean」、這個 clean 是同源盲區的產物、不是真 clean。比沒審更危險：沒審還知道沒查、同源審完誤以為查過了、register 違規帶著「已審查」標籤留在稿件裡。</p>
<h3 id="規範卡越多越以為夠了">規範卡越多、越以為夠了</h3>
<p>blog 累積了 #94 / #147 / #149 一整套 register 審查原則、看起來防護很完整、反而讓人以為「照卡片做就夠」。這次的失敗證明：原則齊備、修法齊備、同源執行仍跨不過上限。原則的數量不等於防線的強度。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 字句層 review：keyword bank 命中是候選、不是判決</a></strong>：#149 揭露「命中後判定會放水」這個現象、本卡是它的上層 —— 揭露放水的根因（判定在品味側、同源自審有結構上限）跟結構解（異源視角、不是更好的判定準則）。#149 的「概念位置判準」是好的判準、但它仍要同源 reviewer 執行品味判斷、本卡指出這一步有上限。</li>
<li><strong><a href="../rule-codification-vs-self-audit/">#147 規範化跟自審是兩種認知任務</a></strong>：#147 講「立規範不等於自審執行」、修法是轉 grep + sweep。本卡補它沒覆蓋的：對 register 違規、grep + sweep 命中了、判定仍跨不過同源盲區。#147 解「自審有沒有做」、本卡解「自審做了、為什麼對 register 仍失效」。</li>
<li><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精修</a></strong>：本卡把 #82 的「字面 vs 行為」推到極限 —— 對 register 違規、連「行為精修」（讀者能不能驗證）都還是品味判斷、需要異源的讀者才感受得到。形式違規停在字面層即可、register 違規連行為層都需要異源。</li>
<li><strong><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review scope 要蓋同類風險區</a></strong>：#95 解 scope 軸（同類違規要 corpus-wide grep）、本卡解 source 軸（同類違規的判定要文體異源）。兩個一起才完整：grep 把候選掃出來（#95）、異源的眼睛判它違不違規（本卡）。這次三個 surface 鏡像內容共享同一違規句型、grep 掃得到、但同源判定全放行 —— scope 對了、source 沒換。</li>
<li><strong><a href="../multi-pass-review-frame-granularity-blindspot/">#114 Multi-pass review 的 frame 顆粒度盲點</a></strong>：#114 解偵測層（展開成 keyword bank）、本卡指出對 register 違規、偵測層做好了、判定層仍需異源 —— frame 顆粒度再細、同源 reviewer 對品味違規仍有盲區。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對某個寫作違規、一直在「換更好的檢查方法」（補 grep / 寫判準 / 做測試）</td>
          <td>停 —— 先問它在品味側還形式側、品味側的解是換 source、不是換 method</td>
      </tr>
      <tr>
          <td>一個違規「偵測明顯（grep 抓得到）」但「判定要讀懂語意」</td>
          <td>偵測可機械化、判定在品味側、別把偵測的容易誤當判定的容易</td>
      </tr>
      <tr>
          <td>多輪 LLM 審查全 clean、但作者以外的人一眼看到違規</td>
          <td>同源盲區的 retro signal、這類違規需要文體異源視角</td>
      </tr>
      <tr>
          <td>reviewer 全是 LLM、prompt 全是「自然語言判準」</td>
          <td>register 層缺異源防線、補人類冷讀或對抗文體 reviewer</td>
      </tr>
      <tr>
          <td>回報「字句層 clean」、但只跑了同源 reviewer</td>
          <td>這個 clean 可能是同源盲區產物、標「register 層未經異源抽查」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：register / 品味類寫作規範（正向陳述、概念前置、喊話、誇飾、口語修辭、文體）的 review 流程設計；AI 輔助寫作的 self-review（最容易在 register 層同源放水）；multi-round-review 的 frame 與資源配置。</li>
<li><strong>不適用</strong>：形式違規（emoji、編號、broken link、frontmatter）—— 這類確定性判定、進工具鏈即可、不需異源、本卡的「同源上限」不適用。</li>
<li><strong>邊界</strong>：「同源自審有上限」不等於「同源自審無用」 —— 同源 reviewer 仍能抓形式違規、抓部分明顯的 register 違規；上限指的是「對 register 違規無法做到窮舉可靠」、所以要補異源、不是廢掉同源。也不等於「只能靠人」 —— 偵測機械化、對抗文體 reviewer、reader-simulation 都能降低對純人工的依賴、只是接受殘餘要異源抽查。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於一次 backend 章節 + skill 的四輪 multi-round-review。流程是 12 個 LLM reviewer、4 輪、frame 涵蓋規範遵循 / 技術 / 一致性 / cadence / reader-simulation / title / self-application / steelman / outbound / cold-read / misuse / regression。Round 1-A 與 Round 3-A 都跑了 <code>compositional-writing</code> 的正向陳述 grep keyword bank、命中了三個 surface 的「不是 X、而是 Y」、全部判成「合規對照」放行、回報「字句層 clean」。</p>
<p>四輪結束、三個 PR 合併之後、使用者讀稿件、一眼指出「外包一塊能力<strong>不是</strong>二元、<strong>而是</strong>有深度的」是否定句型 + 概念後置違規。同一個違規在 0.22 章節、知識卡、skill principle 卡三個 surface 都存在、12 個 LLM reviewer 全漏、使用者一眼抓到。</p>
<p>對應本卡：register 違規的判定在品味側、12 個 LLM reviewer 共享文體直覺、同源自審跨不過盲區；使用者是文體異源的眼睛、所以一眼看到。這次失敗證明 #147 的「轉 grep + sweep」、#149 的「概念位置判定」、補 grep keyword bank —— 三個自動化側的修法疊起來、對 register 違規仍跨不過同源上限。結構解是把異源視角寫進流程、不是再疊第四個自動化側修法。</p>
]]></content:encoded></item><item><title>重點優先陳述是跨語言的資訊結構原則、不是中文句型問題</title><link>https://tarrragon.github.io/blog/report/lead-with-the-point-cross-language/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/lead-with-the-point-cross-language/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>正向陳述優先的本質是資訊結構效率：讀者拿到核心概念的認知步驟越少越好。「不是 X、而是 Y」表達能力差、是因為它讓讀者先處理一個被否定的錯誤理解 X、才拿到正確的 Y —— 重點後置、多繞一步。問題的根本在資訊結構、不在中文特有的句型、也不在主觀品味。&lt;/p>
&lt;p>這個校正很重要、因為它決定了解法的方向：把問題定位成「中文句型」、會去抓句型變體或換語言；定位成「品味」、會去靠主觀判定或異源視角；定位成「資訊結構效率」、才會去檢查「核心概念在不在最前」這個跨語言、可操作的判準。&lt;/p>
&lt;hr>
&lt;h2 id="證偽過的反例假設換語言能打破">證偽過的反例假設：「換語言能打破」&lt;/h2>
&lt;p>一個自然的假設：如果「不是 X、而是 Y」是 LLM 中文訓練語料的高頻行為、那換英文或日文思考就能打破。這個假設值得驗、但證偽了。&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>不是 X、而是 Y&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>英文&lt;/td>
 &lt;td>not X but rather Y、it&amp;rsquo;s not that X but Y、rather than X&lt;/td>
 &lt;td>學術 / 技術修辭傳統的核心愛用句、可能更高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>日文&lt;/td>
 &lt;td>X ではなく Y、A というより、むしろ B&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>LLM 在三種語言都從語料學到對應的高頻版本、換語言不打破、英文甚至可能更嚴重。&lt;/p>
&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>「X、不是 Y」&lt;/td>
 &lt;td>重點 X 先行&lt;/td>
 &lt;td>合法&lt;/td>
 &lt;td>#94&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「不是 X、而是 Y」&lt;/td>
 &lt;td>重點 Y 後置&lt;/td>
 &lt;td>違規&lt;/td>
 &lt;td>#149 的違規例&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>判別是「核心概念在不在最前」、不是「否定的角色」（#149 的「概念位置」其實就是這個、本卡正名它為資訊結構的重點優先）、不是「對照刪不刪」（#94）、不是「句型」。一個資訊結構判準涵蓋全部對照句、且涵蓋所有重點後置變體（與其 X 不如 Y、不只 X 更 Y），而句型 grep 永遠枚舉不完。實證在這次的 POS lint pattern：連接詞清單擴了兩次（而是 → 加「— 是」→ 加「不在…而在」）、每次擴完就暴露下一個變體、仍漏第四個「不是 X、是 Y」頓號版；而「、是」頓號版誤判率高（「不是 A、是 B」多為正常列舉）、刻意不補進 pattern —— 正是判準（重點位置）優於窮舉（連接詞）的活證明。完整修法軌跡見 &lt;a href="../remediation-introduces-sibling-variants/">#167 修法是新違規的來源、且常引入同類變體&lt;/a>。&lt;/p>
&lt;p>這個判準可操作：對每個定義句、找核心概念第一次正面出現的位置、在句首就合規、被擠到「而是」之後就違規。它比「判斷這個否定是建立概念還是對照」客觀得多。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-llm-系統性放水高頻偏置">為什麼 LLM 系統性放水：高頻偏置&lt;/h2>
&lt;p>LLM 從語料學到「不是 X、而是 Y」是高頻、讀起來優雅的表達、於是它的「表達能力評價」把這個句型評為好。問題在高頻不等於資訊結構優 —— 這個句型高頻、是因為它在修辭上有對照張力、好寫好讀；它效率次優、是因為重點後置。兩件事獨立、而 LLM 的評價把前者（高頻 / 優雅）誤當後者（資訊結構好）。&lt;/p>
&lt;p>這個偏置跨語言、且作者與 reviewer 共享（都是 LLM、學同一類語料）。所以 LLM reviewer 讀到這個句型覺得「自然、好」、跳過重點位置判準、走「讀起來優雅就放行」的捷徑。歸因是訓練語料的高頻偏置、比「文體盲區」這個說法更具體可檢驗。&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;/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>human cold-read / 對抗文體 reviewer（&lt;a href="../register-violation-needs-cross-style-eyes/">#165&lt;/a>）&lt;/td>
 &lt;td>補充&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>異源視角是補充、不是唯一解。它補兩種殘餘：強制判準仍漏的、以及無單一重點位置的 register 類（喊話、誇飾 —— 這類回到 #165 的同源上限）。對「重點優先」這個可操作子集、強制執行判準就能抓大部分、不必先跳到「不可機械化、只能靠人」。&lt;/p>
&lt;hr>
&lt;h2 id="存量候選的漸進判讀">存量候選的漸進判讀&lt;/h2>
&lt;p>把重點位置判準升進工具鏈（&lt;code>mdtools lint&lt;/code> 的 POS-negation-lead 警告層、見 &lt;a href="https://tarrragon.github.io/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">markdown-writing-spec&lt;/a> 否定起手候選掃描段）之後、全 &lt;code>content/&lt;/code> 約 300 個重點後置候選一次曝光 —— 它們之前 silent、靠 review 記憶才偶爾 catch。曝光本身就是價值（候選在每次 lint 持續可見、不再靠記憶）、判讀則漸進進行：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>不一次清倉&lt;/strong>：300 裡多數是合法對照（核心概念已先行、「不是 X、而是 Y」只是補充）或 meta / 引用（講此句型的卡與規範），真違規只是子集 —— 一次性全判 ROI 遞減。&lt;/li>
&lt;li>&lt;strong>changed-set scoped 漸進&lt;/strong>：規則讓候選在每次 lint 持續可見、碰到含 POS 警告的檔時用上面三類判準順手判、真違規改重點先行。這是 markdown-writing-spec 位置引用掃描段與否定起手掃描段的存量處理原則。&lt;/li>
&lt;li>&lt;strong>本卡承擔這個漸進工作的 backlog&lt;/strong>：判讀協議（重點位置 + 三類處置）寫在這、未來遇到候選的人讀本卡就知道怎麼判、不需要另開記錄文件。report 卡比單純的記錄文件更有條理 —— 原則、判讀協議、落地狀態收在同一處。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="在語言層找解治不了跨語言的問題">在語言層找解、治不了跨語言的問題&lt;/h3>
&lt;p>把問題定位成「中文句型」、就會去補 grep 抓句型變體、或想換語言打破。句型 grep 枚舉不完、換語言不打破（證偽過）—— 兩條都在語言層用力、而問題在跨語言的資訊結構層。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>正向陳述優先的本質是資訊結構效率：讀者拿到核心概念的認知步驟越少越好。「不是 X、而是 Y」表達能力差、是因為它讓讀者先處理一個被否定的錯誤理解 X、才拿到正確的 Y —— 重點後置、多繞一步。問題的根本在資訊結構、不在中文特有的句型、也不在主觀品味。</p>
<p>這個校正很重要、因為它決定了解法的方向：把問題定位成「中文句型」、會去抓句型變體或換語言；定位成「品味」、會去靠主觀判定或異源視角；定位成「資訊結構效率」、才會去檢查「核心概念在不在最前」這個跨語言、可操作的判準。</p>
<hr>
<h2 id="證偽過的反例假設換語言能打破">證偽過的反例假設：「換語言能打破」</h2>
<p>一個自然的假設：如果「不是 X、而是 Y」是 LLM 中文訓練語料的高頻行為、那換英文或日文思考就能打破。這個假設值得驗、但證偽了。</p>
<table>
  <thead>
      <tr>
          <th>語言</th>
          <th>對應句型</th>
          <th>密度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>中文</td>
          <td>不是 X、而是 Y</td>
          <td>高</td>
      </tr>
      <tr>
          <td>英文</td>
          <td>not X but rather Y、it&rsquo;s not that X but Y、rather than X</td>
          <td>學術 / 技術修辭傳統的核心愛用句、可能更高</td>
      </tr>
      <tr>
          <td>日文</td>
          <td>X ではなく Y、A というより、むしろ B</td>
          <td>高</td>
      </tr>
  </tbody>
</table>
<p>LLM 在三種語言都從語料學到對應的高頻版本、換語言不打破、英文甚至可能更嚴重。</p>
<p>這個證偽的價值不在「結論是否定的」、在它把問題釘到正確的層：如果這個句型跨語言都在、那本質就不在「某種語言的句型」、而在所有語言共享的資訊結構偏好（重點後置讀起來「優雅」、但效率次優）。</p>
<hr>
<h2 id="判別線重點位置統一了對照句的判定">判別線：重點位置統一了對照句的判定</h2>
<p>核心概念在不在句首、是判別對照句合不合規的單一判準：</p>
<table>
  <thead>
      <tr>
          <th>句型</th>
          <th>重點位置</th>
          <th>判定</th>
          <th>既有卡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「X、不是 Y」</td>
          <td>重點 X 先行</td>
          <td>合法</td>
          <td>#94</td>
      </tr>
      <tr>
          <td>「不是 X、而是 Y」</td>
          <td>重點 Y 後置</td>
          <td>違規</td>
          <td>#149 的違規例</td>
      </tr>
  </tbody>
</table>
<p>判別是「核心概念在不在最前」、不是「否定的角色」（#149 的「概念位置」其實就是這個、本卡正名它為資訊結構的重點優先）、不是「對照刪不刪」（#94）、不是「句型」。一個資訊結構判準涵蓋全部對照句、且涵蓋所有重點後置變體（與其 X 不如 Y、不只 X 更 Y），而句型 grep 永遠枚舉不完。實證在這次的 POS lint pattern：連接詞清單擴了兩次（而是 → 加「— 是」→ 加「不在…而在」）、每次擴完就暴露下一個變體、仍漏第四個「不是 X、是 Y」頓號版；而「、是」頓號版誤判率高（「不是 A、是 B」多為正常列舉）、刻意不補進 pattern —— 正是判準（重點位置）優於窮舉（連接詞）的活證明。完整修法軌跡見 <a href="../remediation-introduces-sibling-variants/">#167 修法是新違規的來源、且常引入同類變體</a>。</p>
<p>這個判準可操作：對每個定義句、找核心概念第一次正面出現的位置、在句首就合規、被擠到「而是」之後就違規。它比「判斷這個否定是建立概念還是對照」客觀得多。</p>
<hr>
<h2 id="為什麼-llm-系統性放水高頻偏置">為什麼 LLM 系統性放水：高頻偏置</h2>
<p>LLM 從語料學到「不是 X、而是 Y」是高頻、讀起來優雅的表達、於是它的「表達能力評價」把這個句型評為好。問題在高頻不等於資訊結構優 —— 這個句型高頻、是因為它在修辭上有對照張力、好寫好讀；它效率次優、是因為重點後置。兩件事獨立、而 LLM 的評價把前者（高頻 / 優雅）誤當後者（資訊結構好）。</p>
<p>這個偏置跨語言、且作者與 reviewer 共享（都是 LLM、學同一類語料）。所以 LLM reviewer 讀到這個句型覺得「自然、好」、跳過重點位置判準、走「讀起來優雅就放行」的捷徑。歸因是訓練語料的高頻偏置、比「文體盲區」這個說法更具體可檢驗。</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>文體異源視角</td>
          <td>human cold-read / 對抗文體 reviewer（<a href="../register-violation-needs-cross-style-eyes/">#165</a>）</td>
          <td>補充</td>
      </tr>
  </tbody>
</table>
<p>異源視角是補充、不是唯一解。它補兩種殘餘：強制判準仍漏的、以及無單一重點位置的 register 類（喊話、誇飾 —— 這類回到 #165 的同源上限）。對「重點優先」這個可操作子集、強制執行判準就能抓大部分、不必先跳到「不可機械化、只能靠人」。</p>
<hr>
<h2 id="存量候選的漸進判讀">存量候選的漸進判讀</h2>
<p>把重點位置判準升進工具鏈（<code>mdtools lint</code> 的 POS-negation-lead 警告層、見 <a href="/blog/posts/blog-markdown-%E5%AF%AB%E4%BD%9C%E8%A6%8F%E7%AF%84%E8%88%87-mdtools-%E6%AA%A2%E6%9F%A5/" data-link-title="Blog Markdown 寫作規範與 mdtools 檢查" data-link-desc="本 blog 的 Markdown 排版規範權威契約。涵蓋 H1 禁用、MD024 siblings_only、反釣魚 TLD 校驗、卡片雙向完整性、front matter schema；改規則時要與 scripts/mdtools 實作同步。">markdown-writing-spec</a> 否定起手候選掃描段）之後、全 <code>content/</code> 約 300 個重點後置候選一次曝光 —— 它們之前 silent、靠 review 記憶才偶爾 catch。曝光本身就是價值（候選在每次 lint 持續可見、不再靠記憶）、判讀則漸進進行：</p>
<ul>
<li><strong>不一次清倉</strong>：300 裡多數是合法對照（核心概念已先行、「不是 X、而是 Y」只是補充）或 meta / 引用（講此句型的卡與規範），真違規只是子集 —— 一次性全判 ROI 遞減。</li>
<li><strong>changed-set scoped 漸進</strong>：規則讓候選在每次 lint 持續可見、碰到含 POS 警告的檔時用上面三類判準順手判、真違規改重點先行。這是 markdown-writing-spec 位置引用掃描段與否定起手掃描段的存量處理原則。</li>
<li><strong>本卡承擔這個漸進工作的 backlog</strong>：判讀協議（重點位置 + 三類處置）寫在這、未來遇到候選的人讀本卡就知道怎麼判、不需要另開記錄文件。report 卡比單純的記錄文件更有條理 —— 原則、判讀協議、落地狀態收在同一處。</li>
</ul>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="在語言層找解治不了跨語言的問題">在語言層找解、治不了跨語言的問題</h3>
<p>把問題定位成「中文句型」、就會去補 grep 抓句型變體、或想換語言打破。句型 grep 枚舉不完、換語言不打破（證偽過）—— 兩條都在語言層用力、而問題在跨語言的資訊結構層。</p>
<h3 id="把可操作問題誤診成品味上限">把可操作問題誤診成品味上限</h3>
<p>把「重點位置」這個可操作判準、誤診成「不可機械化的品味、只能靠異源」（<a href="../register-violation-needs-cross-style-eyes/">#165</a> 的過度推論）、就會放棄判準、過度依賴異源。實際上強制執行重點位置判準就能抓大部分 —— 這是把 execution gap（有判準沒執行）誤判成 design 上限（判準不存在）、正是 <a href="../review-miss-diagnose-design-vs-execution-gap/">#153</a> 警告的診斷錯誤。</p>
<h3 id="高頻偏置讓優雅變成放行藉口">高頻偏置讓「優雅」變成放行藉口</h3>
<p>reviewer 用「這句讀起來優雅 / 自然」當放行理由、等於用高頻偏置覆蓋資訊結構判準。優雅與效率是兩件事、要分開問。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../register-violation-needs-cross-style-eyes/">#165 register 違規：偵測可機械化、判定要靠文體異源</a></strong>：本卡是 #165 的上層、並校正它兩個過度推論。#165 揭露「同源 LLM 漏 register 違規」這個現象、有價值；但它把解過度歸到「判定不可機械化、異源是唯一結構解」。本卡指出「不是 X、而是 Y」這個子集的本質是可操作的資訊結構判準（重點位置）、主解是強制執行、異源降為補充。#165 對喊話 / 誇飾這類無單一重點位置的 register 仍成立、本卡聚焦「重點優先」這個可操作子集。</li>
<li><strong><a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 keyword bank 命中是候選、不是判決</a></strong>：#149 的「概念位置判定」其實就是重點位置、本卡把它正名為「資訊結構的重點優先」、並指出它跨語言、可操作。#149 停在「用概念位置判定」、本卡補「為什麼判定會跳過（高頻偏置）」與「怎麼強制執行」。</li>
<li><strong><a href="../remediation-introduces-sibling-variants/">#167 修法是新違規的來源、且常引入同類變體</a></strong>：本卡是靜態原則（句型枚舉不完、判準是重點位置）、#167 是它的過程面 —— 修法 / 補 pattern 的動作反覆暴露同類變體（這次 POS pattern 連接詞清單擴兩次仍漏第四個的軌跡），收斂靠本卡的重點位置判準、不靠窮舉連接詞。</li>
<li><strong><a href="../positive-rewrite-preserves-contrast/">#94 正向改寫要保留對照論據</a></strong>：本卡統一 #94 與 #149 的判別線。#94 的「X、不是 Y」是重點先行（合法）、#149 的「不是 X、而是 Y」是重點後置（違規）—— 兩者的判別線是同一個「重點位置」、不是兩條獨立規則。</li>
<li><strong><a href="../review-miss-diagnose-design-vs-execution-gap/">#153 Review 漏抓先分 design gap 與 execution gap</a></strong>：本卡是 #153 的一個實證 —— #165 把「reviewer 有判準（概念位置）卻沒執行」（execution gap）誤診成「判定不可機械化」（design 上限）。本卡校正回 execution + 可操作判準。</li>
<li><strong><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精修</a></strong>：重點位置是行為層判準（讀者拿到核心要幾個認知步驟）、比字面層（grep「不是…而是」句型）更本質、且跨語言。grep 抓字面句型、重點位置判讀者的認知成本。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對某寫作問題在「語言 / 句型」層找解（換語言、grep 句型變體）</td>
          <td>退一步問「本質是不是跨語言的資訊結構問題」、在語言層找不到根本解</td>
      </tr>
      <tr>
          <td>「不是 X、而是 Y / 與其 X 不如 Y / 不只 X 更 Y」</td>
          <td>找核心概念第一次正面出現的位置、後置就前置</td>
      </tr>
      <tr>
          <td>reviewer 覺得某句「讀起來優雅 / 自然」就放行</td>
          <td>警惕高頻偏置 —— 優雅與資訊結構效率是兩件事、用重點位置判準覆核</td>
      </tr>
      <tr>
          <td>把寫作問題歸到「不可機械化、只能靠人 / 異源」</td>
          <td>先確認是不是有可操作的結構判準被跳過（execution）、別太快宣告 design 上限</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：對照句 / 定義句的重點位置（不是…而是、與其…不如、不只…更）；跨語言寫作；技術 / 教學文（重點優先最該成立的 surface）。</li>
<li><strong>不適用</strong>：敘事 / 懸念類寫作（先鋪陳後揭重點是設計、不是缺陷）；無單一「重點」的純列舉。</li>
<li><strong>邊界</strong>：「重點優先」是可操作判準、但「哪個是重點」仍需理解內容 —— 可操作指「一旦識別重點、位置判定客觀」、不是「全自動無需理解」。對喊話 / 誇飾這類非重點位置的 register、回到 #165 的異源補充。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡觸發於 <a href="../register-violation-needs-cross-style-eyes/">#165</a> 寫完之後、使用者的追問：如果「不是 X、而是 Y」是 LLM 中文訓練出來的行為、換英文或日文思考能不能打破？這個假設的證偽（英 / 日文同樣高頻、英文修辭傳統尤甚）、把問題從「中文句型」釘到「跨語言資訊結構」。使用者並指出真需求是「正面語境描述 + 重點優先陳述」、本質是這個句型表達能力差（重點後置）、不是對抗特定句型。</p>
<p>這校正了 #165 的兩個過度推論：本質可操作（重點位置、跨語言）、不是不可機械化的品味；主解是強制執行重點位置判準、不是異源唯一。記錄這次「把可操作問題誤診成品味上限」的校正 —— 它跟同批 retrospective 第一輪「把問題過度工具化（補 grep）」對稱、兩次都偏離中道（一次過度工具化、一次過度玄學化）、由使用者連續追問拉回（見 memory <code>root-cause-over-toolification</code>）。</p>
]]></content:encoded></item><item><title>修法是新違規的來源、且常引入同類變體</title><link>https://tarrragon.github.io/blog/report/remediation-introduces-sibling-variants/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/remediation-introduces-sibling-variants/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>修法是新違規的來源。改寫一個違規句、補一條 lint 規則、改一個 pattern——這些動作本身會引入新問題，而且引入的常是同一類問題的變體：修掉「不是 X、而是 Y」、就暴露「不是 X — 是 Y」；補一條 pattern 抓某變體、下一個變體又漏。&lt;/p>
&lt;p>兩個推論直接影響 review 流程：review 的 scope 要涵蓋「修法後的產物」、不只「原始內容」；停止判斷不能停在「修完這批」、因為修法本身產生新一批。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼修法引入的是同類變體">為什麼修法引入的是同類變體&lt;/h2>
&lt;p>修法用的是跟原作者同一套文體直覺、同一個對問題的理解。改一個表面實例時，注意力放在「這一處」、而同一個底層成因（重點後置的偏好、某個高頻句型）在別處或下一個變體仍在。修「而是」連接詞、沒動到「重點後置」這個成因，於是「— 是」「不在…而在」這些同成因的近親一個個浮現。補 pattern 也一樣：每補一個連接詞、成因（重點後置）沒被判準收斂、下一個連接詞的變體就漏。&lt;/p>
&lt;p>這跟全新違規不同：全新違規是不同成因，同類變體是同一成因的不同表面。修法最容易產生後者——因為修的人盯著表面、放過成因。&lt;/p>
&lt;hr>
&lt;h2 id="這次的實證同一批-retrospective-反覆出現">這次的實證（同一批 retrospective 反覆出現）&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>Round 1 修字句層&lt;/td>
 &lt;td>Round 2 抓到新引入的跨 surface 近逐字&lt;/td>
 &lt;td>下一輪不同 frame&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Round 3 修 80/20 段&lt;/td>
 &lt;td>Round 4 抓到修法又植入一處新近逐字&lt;/td>
 &lt;td>下一輪 frame&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補 POS pattern「而是」&lt;/td>
 &lt;td>漏「不是 X — 是 Y」&lt;/td>
 &lt;td>人複核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補「— 是」變體&lt;/td>
 &lt;td>漏「不在 X、而在 Y」&lt;/td>
 &lt;td>對抗文體 agent&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補「不在…而在」&lt;/td>
 &lt;td>漏「不是 X、是 Y」頓號版&lt;/td>
 &lt;td>人複核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修觸發句「不是二元、而是有深度」（三 surface）&lt;/td>
 &lt;td>漏第 2 觸發句「不是吹噓技術 — 是…」（不同連接詞變體）&lt;/td>
 &lt;td>人複核&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每一次修法都暴露下一個同類變體。POS pattern 的連接詞清單擴了兩次（而是 → 加「— 是」→ 加「不在…而在」）、每次擴完就暴露下一個變體、第四個「、是」頓號版發現後決定不補——這正是「打地鼠」：表面層追不上成因層。&lt;/p>
&lt;hr>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="review-scope-涵蓋修法產物">review scope 涵蓋修法產物&lt;/h3>
&lt;p>修完一批不假設它乾淨。修法產物要重新過一次同樣的 frame——尤其修的是「會引入同類變體」的改寫或規則。這是 &lt;a href="../multi-pass-scope-must-cover-risk-zone/">#95&lt;/a>「scope 蓋同類風險區」的時間軸延伸：修法後的產物也是同類風險區、且是剛產生的。&lt;/p>
&lt;h3 id="同類變體靠判準收斂不靠窮舉">同類變體靠判準收斂、不靠窮舉&lt;/h3>
&lt;p>成因層的判準（&lt;a href="../lead-with-the-point-cross-language/">#166&lt;/a> 的「核心概念在不在最前」）一次涵蓋所有連接詞變體；表面層的 pattern 永遠枚舉不完（擴兩次仍冒出第四個變體）。所以工具鏈的 pattern 定位是「曝光候選」、收斂靠判準——pattern 漏一個變體只是讓候選 silent、判準仍能在人讀到時抓出。有些變體（「、是」頓號版）誤判率高、刻意不補進 pattern、正是「判準優於窮舉」的活證明。&lt;/p>
&lt;h3 id="停止判斷含修法產物">停止判斷含修法產物&lt;/h3>
&lt;p>「修完這批違規」不是停止訊號。修法產生新一批、停止判斷要看「frame 涵蓋 + 判準到位」（&lt;a href="../cross-round-review-stopping-signal/">#148&lt;/a> 的延伸）、不看「這批修完了」。&lt;/p>
&lt;hr>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;h3 id="修完回報-clean新引入的違規-silent">修完回報 clean、新引入的違規 silent&lt;/h3>
&lt;p>修法後直接回報「修好了」、不重審修法產物——新引入的同類變體帶著「已修」標籤留下。這次 Round 2 / Round 4 抓到的、都是前一輪「修完回報 clean」後才被下一輪 frame 揭出的。&lt;/p>
&lt;h3 id="補-pattern-永遠追不上變體">補 pattern 永遠追不上變體&lt;/h3>
&lt;p>把希望放在「補完所有 pattern 變體」上、就會無限打地鼠——每補一個、下一個浮現。連接詞清單擴兩次仍冒出第四個變體（「、是」），證明表面層的窮舉追不上成因層。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="../lead-with-the-point-cross-language/">#166 重點優先陳述是跨語言的資訊結構原則&lt;/a>&lt;/strong>：#166 講「句型 grep 枚舉不完、判準是重點位置」（靜態原則），本卡是它的過程面——修法 / 補 pattern 的動作會反覆暴露同類變體，正是「枚舉不完」在 remediation 過程的動態展現。#166 self-case 記了完整的 pattern 連接詞擴展軌跡。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review scope 要蓋同類風險區&lt;/a>&lt;/strong>：#95 是空間軸（同類違規分布整個 corpus），本卡是時間軸（修法後的產物是剛產生的同類風險區）——兩個一起才是完整 scope：橫向蓋 corpus、縱向蓋修法產物。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../review-miss-diagnose-design-vs-execution-gap/">#153 Review 漏抓先分 design gap 與 execution gap&lt;/a>&lt;/strong>：本卡補一類 gap 的來源——修法本身引入的新 gap。修法後沒重審，是把「修法產物」排除在 scope 外的 execution gap。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../cross-round-review-stopping-signal/">#148 跨輪 review 停止訊號是 frame 涵蓋&lt;/a>&lt;/strong>：本卡補「修法產生新批」這個維度——停止判斷不能停在「修完這批」、要含修法產物的重審。&lt;/li>
&lt;li>&lt;strong>&lt;a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 keyword bank 命中是候選、不是判決&lt;/a>&lt;/strong>：補 pattern 抓變體是偵測層、收斂靠判準是判定層——本卡的「靠判準不靠窮舉」是 #149「判定優先」在 remediation 的應用。&lt;/li>
&lt;/ul>
&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>剛修完一批就回報「clean / 修好了」&lt;/td>
 &lt;td>停 —— 修法產物還沒重審、新引入的同類變體可能 silent&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修的是「同一類問題」（批量改寫 / 補規則 / 改 pattern）&lt;/td>
 &lt;td>預期它引入同類變體、修完重掃同類、別假設乾淨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>補 pattern / 規則後沒重掃同類風險區&lt;/td>
 &lt;td>補完只解了已知變體、下一個變體要靠重掃或判準 catch&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>把希望放在「補完所有 pattern 變體」&lt;/td>
 &lt;td>打地鼠訊號 —— 表面層追不上成因、回到判準（成因層）收斂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>停止判斷的理由是「這批修完了」&lt;/td>
 &lt;td>不是停止訊號 —— 修法產生新批、看 frame 涵蓋 + 判準到位&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="適用範圍與邊界">適用範圍與邊界&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>適用&lt;/strong>：批量改寫違規、補 lint 規則 / pattern、multi-round-review 的修法階段、任何「修一類問題」的 remediation。&lt;/li>
&lt;li>&lt;strong>不適用&lt;/strong>：修單一孤立 bug（無同類變體、修完即止）；確定性違規的工具鏈修法（emoji / broken link，補一條規則就窮舉完、無成因層變體）。&lt;/li>
&lt;li>&lt;strong>邊界&lt;/strong>：「修法引入同類變體」不等於「不該修」——該修、但修完要把修法產物納入下一輪 scope，且收斂靠成因層判準不靠表面窮舉。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源&lt;/h2>
&lt;p>本卡抽自一次 backend + skill 的 register 違規 retrospective。整個過程是一連串修法、每次修法都暴露下一個同類變體：四輪 multi-round-review 每輪修完、下一輪不同 frame 都抓到前輪修法引入的新近逐字；POS-negation-lead pattern 的連接詞清單擴了兩次（而是 → 加「— 是」→ 加「不在…而在」）、每次擴完就暴露下一個變體、第四個「、是」頓號版發現後決定不補；修第 1 觸發句「不是二元、而是有深度」後、漏了第 2 觸發句「不是吹噓技術 — 是…」（不同連接詞變體），由使用者複核才抓到。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>修法是新違規的來源。改寫一個違規句、補一條 lint 規則、改一個 pattern——這些動作本身會引入新問題，而且引入的常是同一類問題的變體：修掉「不是 X、而是 Y」、就暴露「不是 X — 是 Y」；補一條 pattern 抓某變體、下一個變體又漏。</p>
<p>兩個推論直接影響 review 流程：review 的 scope 要涵蓋「修法後的產物」、不只「原始內容」；停止判斷不能停在「修完這批」、因為修法本身產生新一批。</p>
<hr>
<h2 id="為什麼修法引入的是同類變體">為什麼修法引入的是同類變體</h2>
<p>修法用的是跟原作者同一套文體直覺、同一個對問題的理解。改一個表面實例時，注意力放在「這一處」、而同一個底層成因（重點後置的偏好、某個高頻句型）在別處或下一個變體仍在。修「而是」連接詞、沒動到「重點後置」這個成因，於是「— 是」「不在…而在」這些同成因的近親一個個浮現。補 pattern 也一樣：每補一個連接詞、成因（重點後置）沒被判準收斂、下一個連接詞的變體就漏。</p>
<p>這跟全新違規不同：全新違規是不同成因，同類變體是同一成因的不同表面。修法最容易產生後者——因為修的人盯著表面、放過成因。</p>
<hr>
<h2 id="這次的實證同一批-retrospective-反覆出現">這次的實證（同一批 retrospective 反覆出現）</h2>
<table>
  <thead>
      <tr>
          <th>修法動作</th>
          <th>引入 / 暴露的同類變體</th>
          <th>誰抓到</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Round 1 修字句層</td>
          <td>Round 2 抓到新引入的跨 surface 近逐字</td>
          <td>下一輪不同 frame</td>
      </tr>
      <tr>
          <td>Round 3 修 80/20 段</td>
          <td>Round 4 抓到修法又植入一處新近逐字</td>
          <td>下一輪 frame</td>
      </tr>
      <tr>
          <td>補 POS pattern「而是」</td>
          <td>漏「不是 X — 是 Y」</td>
          <td>人複核</td>
      </tr>
      <tr>
          <td>補「— 是」變體</td>
          <td>漏「不在 X、而在 Y」</td>
          <td>對抗文體 agent</td>
      </tr>
      <tr>
          <td>補「不在…而在」</td>
          <td>漏「不是 X、是 Y」頓號版</td>
          <td>人複核</td>
      </tr>
      <tr>
          <td>修觸發句「不是二元、而是有深度」（三 surface）</td>
          <td>漏第 2 觸發句「不是吹噓技術 — 是…」（不同連接詞變體）</td>
          <td>人複核</td>
      </tr>
  </tbody>
</table>
<p>每一次修法都暴露下一個同類變體。POS pattern 的連接詞清單擴了兩次（而是 → 加「— 是」→ 加「不在…而在」）、每次擴完就暴露下一個變體、第四個「、是」頓號版發現後決定不補——這正是「打地鼠」：表面層追不上成因層。</p>
<hr>
<h2 id="理想做法">理想做法</h2>
<h3 id="review-scope-涵蓋修法產物">review scope 涵蓋修法產物</h3>
<p>修完一批不假設它乾淨。修法產物要重新過一次同樣的 frame——尤其修的是「會引入同類變體」的改寫或規則。這是 <a href="../multi-pass-scope-must-cover-risk-zone/">#95</a>「scope 蓋同類風險區」的時間軸延伸：修法後的產物也是同類風險區、且是剛產生的。</p>
<h3 id="同類變體靠判準收斂不靠窮舉">同類變體靠判準收斂、不靠窮舉</h3>
<p>成因層的判準（<a href="../lead-with-the-point-cross-language/">#166</a> 的「核心概念在不在最前」）一次涵蓋所有連接詞變體；表面層的 pattern 永遠枚舉不完（擴兩次仍冒出第四個變體）。所以工具鏈的 pattern 定位是「曝光候選」、收斂靠判準——pattern 漏一個變體只是讓候選 silent、判準仍能在人讀到時抓出。有些變體（「、是」頓號版）誤判率高、刻意不補進 pattern、正是「判準優於窮舉」的活證明。</p>
<h3 id="停止判斷含修法產物">停止判斷含修法產物</h3>
<p>「修完這批違規」不是停止訊號。修法產生新一批、停止判斷要看「frame 涵蓋 + 判準到位」（<a href="../cross-round-review-stopping-signal/">#148</a> 的延伸）、不看「這批修完了」。</p>
<hr>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<h3 id="修完回報-clean新引入的違規-silent">修完回報 clean、新引入的違規 silent</h3>
<p>修法後直接回報「修好了」、不重審修法產物——新引入的同類變體帶著「已修」標籤留下。這次 Round 2 / Round 4 抓到的、都是前一輪「修完回報 clean」後才被下一輪 frame 揭出的。</p>
<h3 id="補-pattern-永遠追不上變體">補 pattern 永遠追不上變體</h3>
<p>把希望放在「補完所有 pattern 變體」上、就會無限打地鼠——每補一個、下一個浮現。連接詞清單擴兩次仍冒出第四個變體（「、是」），證明表面層的窮舉追不上成因層。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li><strong><a href="../lead-with-the-point-cross-language/">#166 重點優先陳述是跨語言的資訊結構原則</a></strong>：#166 講「句型 grep 枚舉不完、判準是重點位置」（靜態原則），本卡是它的過程面——修法 / 補 pattern 的動作會反覆暴露同類變體，正是「枚舉不完」在 remediation 過程的動態展現。#166 self-case 記了完整的 pattern 連接詞擴展軌跡。</li>
<li><strong><a href="../multi-pass-scope-must-cover-risk-zone/">#95 Multi-pass review scope 要蓋同類風險區</a></strong>：#95 是空間軸（同類違規分布整個 corpus），本卡是時間軸（修法後的產物是剛產生的同類風險區）——兩個一起才是完整 scope：橫向蓋 corpus、縱向蓋修法產物。</li>
<li><strong><a href="../review-miss-diagnose-design-vs-execution-gap/">#153 Review 漏抓先分 design gap 與 execution gap</a></strong>：本卡補一類 gap 的來源——修法本身引入的新 gap。修法後沒重審，是把「修法產物」排除在 scope 外的 execution gap。</li>
<li><strong><a href="../cross-round-review-stopping-signal/">#148 跨輪 review 停止訊號是 frame 涵蓋</a></strong>：本卡補「修法產生新批」這個維度——停止判斷不能停在「修完這批」、要含修法產物的重審。</li>
<li><strong><a href="../keyword-bank-hit-is-candidate-not-verdict/">#149 keyword bank 命中是候選、不是判決</a></strong>：補 pattern 抓變體是偵測層、收斂靠判準是判定層——本卡的「靠判準不靠窮舉」是 #149「判定優先」在 remediation 的應用。</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>徵兆</th>
          <th>該做的行動</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>剛修完一批就回報「clean / 修好了」</td>
          <td>停 —— 修法產物還沒重審、新引入的同類變體可能 silent</td>
      </tr>
      <tr>
          <td>修的是「同一類問題」（批量改寫 / 補規則 / 改 pattern）</td>
          <td>預期它引入同類變體、修完重掃同類、別假設乾淨</td>
      </tr>
      <tr>
          <td>補 pattern / 規則後沒重掃同類風險區</td>
          <td>補完只解了已知變體、下一個變體要靠重掃或判準 catch</td>
      </tr>
      <tr>
          <td>把希望放在「補完所有 pattern 變體」</td>
          <td>打地鼠訊號 —— 表面層追不上成因、回到判準（成因層）收斂</td>
      </tr>
      <tr>
          <td>停止判斷的理由是「這批修完了」</td>
          <td>不是停止訊號 —— 修法產生新批、看 frame 涵蓋 + 判準到位</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="適用範圍與邊界">適用範圍與邊界</h2>
<ul>
<li><strong>適用</strong>：批量改寫違規、補 lint 規則 / pattern、multi-round-review 的修法階段、任何「修一類問題」的 remediation。</li>
<li><strong>不適用</strong>：修單一孤立 bug（無同類變體、修完即止）；確定性違規的工具鏈修法（emoji / broken link，補一條規則就窮舉完、無成因層變體）。</li>
<li><strong>邊界</strong>：「修法引入同類變體」不等於「不該修」——該修、但修完要把修法產物納入下一輪 scope，且收斂靠成因層判準不靠表面窮舉。</li>
</ul>
<hr>
<h2 id="self-case本卡的觸發來源">Self-case：本卡的觸發來源</h2>
<p>本卡抽自一次 backend + skill 的 register 違規 retrospective。整個過程是一連串修法、每次修法都暴露下一個同類變體：四輪 multi-round-review 每輪修完、下一輪不同 frame 都抓到前輪修法引入的新近逐字；POS-negation-lead pattern 的連接詞清單擴了兩次（而是 → 加「— 是」→ 加「不在…而在」）、每次擴完就暴露下一個變體、第四個「、是」頓號版發現後決定不補；修第 1 觸發句「不是二元、而是有深度」後、漏了第 2 觸發句「不是吹噓技術 — 是…」（不同連接詞變體），由使用者複核才抓到。</p>
<p>這條軌跡共同指向一個教訓：修法是新違規的來源、且引入的是同類變體；表面層的 pattern 窮舉追不上成因層、收斂要靠判準（<a href="../lead-with-the-point-cross-language/">#166</a> 的重點位置）、review scope 要把修法產物納進來。</p>
]]></content:encoded></item><item><title>多輪審查要有冷讀者 frame：知情 reviewer 看不見行話洩漏</title><link>https://tarrragon.github.io/blog/report/cold-reader-frame-vs-informed-reviewer/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cold-reader-frame-vs-informed-reviewer/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡是一次 multi-round-review 漏抓的 self-retrospective 抽出。限制：樣本為 1 次 review（til/terms 14 卡），「知情 vs 冷讀」二分基於單次觀察；修法（補冷讀 frame）的有效性已由同批內容的後續冷讀驗證初步確認，但未跨多批驗證。讀者把它當「review 讀者模擬的一個必查維度」，不當已驗證流程。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>多輪審查模擬讀者時，要區分兩種讀者，分開跑：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>知情讀者（旅程 frame）&lt;/strong>：讀完全部、依學習路線走，看入口判讀與內容門檻。&lt;/li>
&lt;li>&lt;strong>冷讀者（零脈絡單卡落地 frame）&lt;/strong>：假裝經搜尋或他人貼連結，直接落在單一篇章，沒讀過其他篇與索引。&lt;/li>
&lt;/ul>
&lt;p>關鍵差別：&lt;strong>知情 reviewer 會自動腦補脈絡，因此結構性看不見「洩漏撰寫者預設前提的行話」&lt;/strong>——未定義就出現的「家族」「上述」「如前所述」「本文前面提到」。只有零脈絡冷讀者會立刻問「這裡突然冒出的 X 是什麼」。&lt;/p>
&lt;p>原子化 / Zettelkasten / glossary / 任何可被直連或搜尋單獨抵達的內容，冷讀 frame 為必備，不可只靠旅程 frame。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>til/terms 是一組 14 張互連的術語卡。經 3 輪多輪審查（含「讀者旅程」frame），三個 reviewer 全給 A、零必修。但每張卡都用「連到家族 / 概念家族」這個只有撰寫者懂的詞——讀者從搜尋落在單卡時，根本不知「家族」指什麼。知情 reviewer 讀完全部後腦補了「這是一組詞」，沒察覺這個洩漏。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>把 Reader simulation 拆成兩個子 frame：旅程（知情讀者走路線）與冷讀落地（單卡零脈絡）。冷讀 frame 逐篇問三件事：我知道為何讀這篇嗎？有沒有未在本篇定義的預設脈絡詞？我能不能從這篇找到回路？原子內容兩個 frame 都要跑。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;p>只跑旅程 frame，知情 reviewer 全 A 放行，行話會留到讀者冷讀單卡時才卡住——而那時內容已發佈，回報後得回頭改全部卡。把「讀完全部的 reviewer 覺得通順」當成「冷讀者也通順」，是把 informed 的腦補誤當成內容自包含。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>內容是原子 / 可被搜尋或直連單篇落地（術語卡、glossary、知識卡）。&lt;/li>
&lt;li>reviewer 是「讀完全部」才判讀旅程，沒人模擬「只讀這一篇」。&lt;/li>
&lt;li>文中出現「家族 / 本盒 / 上述 / 如前所述 / 前面提到」這類需要前文才懂的詞。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>本檢討已回饋到 multi-round-review skill（Round 2 新增 B′ 冷讀 frame）。寫作順序上 skill 修改先於本報告，屬例外；正序是報告（來源）→ skill 改進 → 下游。&lt;/p>&lt;/blockquote></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡是一次 multi-round-review 漏抓的 self-retrospective 抽出。限制：樣本為 1 次 review（til/terms 14 卡），「知情 vs 冷讀」二分基於單次觀察；修法（補冷讀 frame）的有效性已由同批內容的後續冷讀驗證初步確認，但未跨多批驗證。讀者把它當「review 讀者模擬的一個必查維度」，不當已驗證流程。</p>
<h2 id="核心原則">核心原則</h2>
<p>多輪審查模擬讀者時，要區分兩種讀者，分開跑：</p>
<ul>
<li><strong>知情讀者（旅程 frame）</strong>：讀完全部、依學習路線走，看入口判讀與內容門檻。</li>
<li><strong>冷讀者（零脈絡單卡落地 frame）</strong>：假裝經搜尋或他人貼連結，直接落在單一篇章，沒讀過其他篇與索引。</li>
</ul>
<p>關鍵差別：<strong>知情 reviewer 會自動腦補脈絡，因此結構性看不見「洩漏撰寫者預設前提的行話」</strong>——未定義就出現的「家族」「上述」「如前所述」「本文前面提到」。只有零脈絡冷讀者會立刻問「這裡突然冒出的 X 是什麼」。</p>
<p>原子化 / Zettelkasten / glossary / 任何可被直連或搜尋單獨抵達的內容，冷讀 frame 為必備，不可只靠旅程 frame。</p>
<h2 id="情境">情境</h2>
<p>til/terms 是一組 14 張互連的術語卡。經 3 輪多輪審查（含「讀者旅程」frame），三個 reviewer 全給 A、零必修。但每張卡都用「連到家族 / 概念家族」這個只有撰寫者懂的詞——讀者從搜尋落在單卡時，根本不知「家族」指什麼。知情 reviewer 讀完全部後腦補了「這是一組詞」，沒察覺這個洩漏。</p>
<h2 id="理想做法">理想做法</h2>
<p>把 Reader simulation 拆成兩個子 frame：旅程（知情讀者走路線）與冷讀落地（單卡零脈絡）。冷讀 frame 逐篇問三件事：我知道為何讀這篇嗎？有沒有未在本篇定義的預設脈絡詞？我能不能從這篇找到回路？原子內容兩個 frame 都要跑。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<p>只跑旅程 frame，知情 reviewer 全 A 放行，行話會留到讀者冷讀單卡時才卡住——而那時內容已發佈，回報後得回頭改全部卡。把「讀完全部的 reviewer 覺得通順」當成「冷讀者也通順」，是把 informed 的腦補誤當成內容自包含。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>內容是原子 / 可被搜尋或直連單篇落地（術語卡、glossary、知識卡）。</li>
<li>reviewer 是「讀完全部」才判讀旅程，沒人模擬「只讀這一篇」。</li>
<li>文中出現「家族 / 本盒 / 上述 / 如前所述 / 前面提到」這類需要前文才懂的詞。</li>
</ul>
<blockquote>
<p>本檢討已回饋到 multi-round-review skill（Round 2 新增 B′ 冷讀 frame）。寫作順序上 skill 修改先於本報告，屬例外；正序是報告（來源）→ skill 改進 → 下游。</p></blockquote>
]]></content:encoded></item><item><title>原子筆記要有向上的議題入口：讀者要知道為何讀這張、何時會撞到</title><link>https://tarrragon.github.io/blog/report/atomic-note-needs-situational-entry/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/atomic-note-needs-situational-entry/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自一次術語卡撰寫的檢討（til/terms 14 卡）。限制：單一案例、單一內容類型（跨領域術語）；「議題 hub + 原子卡」兩層結構對其他內容類型的適用性未驗證。與既有 &lt;a href="../teaching-completeness-by-learner-journey/">教材完整性要用讀者旅程驗證&lt;/a> 區分——那談課程級的學習路線，本卡談單張原子筆記的進入動機與其上層議題。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>承載知識的原子筆記&lt;strong>不是字典條目&lt;/strong>。字典回答「這個詞是什麼」；承載知識的卡要回答「&lt;strong>你在討論什麼、撞到什麼問題，才需要這個知識&lt;/strong>」。&lt;/p>
&lt;p>撰寫者有預設的情境與前提，讀者沒有。所以每張原子卡（或它的上層）要提供進入動機：從情境進入，而非劈頭給定義。具體做法是建一層&lt;strong>議題 hub&lt;/strong>——以讀者會遇到的問題為標題（「測試紅燈不一定是真的壞」「告警太多反而沒人看」），討論議題、再分流到底下的術語卡；術語卡頂部回指它出自哪個議題，讓搜尋直接落地者也有回路。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>til/terms 的術語卡初版，每張都從定義開頭：「alert fatigue 指誤報太多、人對告警麻木……」。內容正確，但讀者不知道自己&lt;strong>為什麼&lt;/strong>會需要這個詞、&lt;strong>何時&lt;/strong>會撞到——卡片成了字典。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>為每個分支建議題 hub（情境入口），從「你遇到什麼問題」進入、再導向該讀的術語卡；術語卡頂部加一行回指議題（「這個詞出現在『X』這個問題裡」）。形成「議題 hub（情境）→ 原子卡（定義）」兩層，讀者由動機而非定義進入。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;p>卡淪為字典：讀者沒有觸發點，搜尋落地後讀完仍不知這對他有何用，知識無法在「對的時機」被取用。承載知識的內容若只有定義層、缺議題層，就退化成可被任何字典取代的條目，失去 blog 累積知識的價值。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>卡片開頭是「X 指……」的定義句，沒有情境鋪陳。&lt;/li>
&lt;li>問「讀者為什麼要讀這張卡」答不出來，或答案是「為了知道定義」。&lt;/li>
&lt;li>一組原子卡彼此互連，卻沒有上層的議題導讀帶讀者進入。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自一次術語卡撰寫的檢討（til/terms 14 卡）。限制：單一案例、單一內容類型（跨領域術語）；「議題 hub + 原子卡」兩層結構對其他內容類型的適用性未驗證。與既有 <a href="../teaching-completeness-by-learner-journey/">教材完整性要用讀者旅程驗證</a> 區分——那談課程級的學習路線，本卡談單張原子筆記的進入動機與其上層議題。</p>
<h2 id="核心原則">核心原則</h2>
<p>承載知識的原子筆記<strong>不是字典條目</strong>。字典回答「這個詞是什麼」；承載知識的卡要回答「<strong>你在討論什麼、撞到什麼問題，才需要這個知識</strong>」。</p>
<p>撰寫者有預設的情境與前提，讀者沒有。所以每張原子卡（或它的上層）要提供進入動機：從情境進入，而非劈頭給定義。具體做法是建一層<strong>議題 hub</strong>——以讀者會遇到的問題為標題（「測試紅燈不一定是真的壞」「告警太多反而沒人看」），討論議題、再分流到底下的術語卡；術語卡頂部回指它出自哪個議題，讓搜尋直接落地者也有回路。</p>
<h2 id="情境">情境</h2>
<p>til/terms 的術語卡初版，每張都從定義開頭：「alert fatigue 指誤報太多、人對告警麻木……」。內容正確，但讀者不知道自己<strong>為什麼</strong>會需要這個詞、<strong>何時</strong>會撞到——卡片成了字典。</p>
<h2 id="理想做法">理想做法</h2>
<p>為每個分支建議題 hub（情境入口），從「你遇到什麼問題」進入、再導向該讀的術語卡；術語卡頂部加一行回指議題（「這個詞出現在『X』這個問題裡」）。形成「議題 hub（情境）→ 原子卡（定義）」兩層，讀者由動機而非定義進入。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<p>卡淪為字典：讀者沒有觸發點，搜尋落地後讀完仍不知這對他有何用，知識無法在「對的時機」被取用。承載知識的內容若只有定義層、缺議題層，就退化成可被任何字典取代的條目，失去 blog 累積知識的價值。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>卡片開頭是「X 指……」的定義句，沒有情境鋪陳。</li>
<li>問「讀者為什麼要讀這張卡」答不出來，或答案是「為了知道定義」。</li>
<li>一組原子卡彼此互連，卻沒有上層的議題導讀帶讀者進入。</li>
</ul>
]]></content:encoded></item><item><title>Description 是未來自己的 recall trigger、不是文章摘要</title><link>https://tarrragon.github.io/blog/report/description-as-recall-trigger/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/description-as-recall-trigger/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自一次三篇 macOS 系統管理文章的多輪審查。審查修完 32 項 finding 後，作者發現 description 雖符合格式規範（30-150 字、非空），語意卻只是摘要——未來回顧列表時無法判斷「何時該重讀」。限制：單一案例（操作型文章），其他類型（教學模組、report 卡、知識卡）的 description 可能有不同的 recall 結構。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>文章的 &lt;code>description&lt;/code> 欄位是寫給&lt;strong>未來自己&lt;/strong>的 recall trigger，不是寫給搜尋引擎的摘要。它要回答的問題是：「我在什麼情境下、遇到什麼問題，會需要回來讀這篇？」&lt;/p>
&lt;p>類比：Claude Code skill 的 &lt;code>description&lt;/code> 讓系統在對話中自動判斷「要不要載入這個 skill」。文章的 description 讓未來的自己在掃列表時自動判斷「要不要進去讀」。兩者目的相同——降低 recall 的認知成本。&lt;/p>
&lt;p>摘要式 description 只回答「這篇&lt;strong>在講什麼&lt;/strong>」（內容索引）；recall trigger 回答「&lt;strong>你在什麼情境下需要這篇&lt;/strong>」（情境索引）。前者是被動的——搜尋命中後才看；後者是主動的——掃列表就能決定。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>三篇 macOS 文章（新機設定 / 磁碟診斷 / App 佔用報告）的 description 寫法：&lt;/p>
&lt;ul>
&lt;li>原本：「從一台 30G 餘裕在幾小時內歸零的 Mac，記錄一套先看快照、再用實際佔用排查的磁碟診斷順序……」&lt;/li>
&lt;li>問題：這是&lt;strong>內容摘要&lt;/strong>——讀者已經知道要找「磁碟診斷」才會搜到它，description 只重述他已預期的內容，沒有增量資訊。&lt;/li>
&lt;li>理想：description 應該告訴未來的自己「你什麼時候會需要回來」——例如「磁碟莫名滿載時的排查起手順序、避開 sparse 假大小陷阱、以及用 tmutil 判讀快照是否為元兇的方法」。&lt;/li>
&lt;/ul>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>description 撰寫時問自己三個問題：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>我未來會在什麼情境下需要這篇？&lt;/strong>（觸發條件）&lt;/li>
&lt;li>&lt;strong>這篇給我的關鍵判讀 / 操作是什麼？&lt;/strong>（帶走的能力）&lt;/li>
&lt;li>&lt;strong>不讀這篇我會踩什麼坑？&lt;/strong>（省下的試錯）&lt;/li>
&lt;/ol>
&lt;p>三者至少涵蓋一個。格式不重要，重要的是 description 讀完後能判斷「現在要不要進去」。&lt;/p>
&lt;p>反例——以下句型通常是摘要不是 trigger：&lt;/p>
&lt;ul>
&lt;li>「記錄了 X 的過程」（日記式）&lt;/li>
&lt;li>「介紹 X 的做法」（教科書式）&lt;/li>
&lt;li>「從 X 事件整理出 Y」（報告式）&lt;/li>
&lt;/ul>
&lt;p>這些句型把 description 當後設描述（meta-description of the article），而不是情境描述（description of when you need it）。&lt;/p>
&lt;h3 id="寫法準則精準無假設無修辭">寫法準則：精準、無假設、無修辭&lt;/h3>
&lt;p>description 給人判斷也給機器（AI）判斷，每個 token 都要有資訊量。三個不要：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>不要操作細節&lt;/strong>——指令、參數、具體步驟留在內文。description 幫人決定「要不要點進去」，不是把文章壓縮塞進來。&lt;/li>
&lt;li>&lt;strong>不要假設前提&lt;/strong>——「想不起來」「卻發現」「卻不確定」「不知從何下手」這些描述讀者的困境，不是資訊。直接說用途。&lt;/li>
&lt;li>&lt;strong>不要情緒修辭&lt;/strong>——「別被騙」「打架」「空轉」「踩坑」這些不精準。用事實陳述取代。&lt;/li>
&lt;li>&lt;strong>不要內嵌數字&lt;/strong>——「三項基礎建設」「6 段結構」「兩個陷阱」把成員數烤進 description，內文增刪就要同步改。description 描述文章的功能（解什麼問題），不描述文章的結構（幾段幾項）。理想的 description 無論內文怎麼調整都不用變動。同 &lt;a href="../name-collections-by-role-not-count/">#156 集合命名不內嵌數量&lt;/a>。&lt;/li>
&lt;/ol>
&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>想不起 figure shortcode 還是 Markdown 時查這篇&lt;/td>
 &lt;td>Hugo 文章插圖的寫法與路徑規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>別被 fail 訊息指的 test 騙&lt;/td>
 &lt;td>fail 位置看 &lt;code>+N -1&lt;/code> 累計而非訊息標示的 test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>套模板每篇結構都打架時回來&lt;/td>
 &lt;td>migration playbook 的結構選型流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>三項基礎建設的依賴順序&lt;/td>
 &lt;td>底層基礎建設的依賴順序（數字隨內容變會過時）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>150 字上限不是要塞滿的配額。短能到位就短。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>列表頁的 description 變成一片「記錄了…」「整理出…」的重複句型，掃不出差異，每篇都要點進去才知道需不需要&lt;/li>
&lt;li>日後同類情境再發生時，想不起來自己寫過、重新搜或重新踩坑&lt;/li>
&lt;li>blog 的知識累積效益被 recall 成本吃掉——寫了等於沒寫&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>description 的主詞是「本文 / 這篇 / 記錄」→ 可能是摘要不是 trigger&lt;/li>
&lt;li>description 刪掉後，只看 title 就能猜出 description 的全部內容 → 沒有增量&lt;/li>
&lt;li>掃列表時無法在 3 秒內判斷「這篇跟我現在的問題有沒有關」→ trigger 失敗&lt;/li>
&lt;/ul>
&lt;h2 id="跟其他原則的關係">跟其他原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="../atomic-note-needs-situational-entry/">#169 原子筆記要有向上的議題入口&lt;/a>：同根因——讀者（含未來的自己）需要「情境入口」而非「定義入口」。#169 談卡片正文的進入動機，本卡談 frontmatter description 的進入動機，是同一原則在不同 surface 的體現。&lt;/li>
&lt;li>&lt;a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證&lt;/a>：讀者旅程的第一站是列表頁的 description——旅程驗證如果從「已進入文章」開始，就跳過了「要不要進入」的判斷點。&lt;/li>
&lt;li>&lt;a href="../audience-fork-before-jargon-wall/">#159 入口分流要放在詞彙牆之前&lt;/a>：description 是文章的入口分流欄位，分流依據應是讀者的情境而非文章的內容結構。&lt;/li>
&lt;/ul>
&lt;hr></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自一次三篇 macOS 系統管理文章的多輪審查。審查修完 32 項 finding 後，作者發現 description 雖符合格式規範（30-150 字、非空），語意卻只是摘要——未來回顧列表時無法判斷「何時該重讀」。限制：單一案例（操作型文章），其他類型（教學模組、report 卡、知識卡）的 description 可能有不同的 recall 結構。</p>
<h2 id="核心原則">核心原則</h2>
<p>文章的 <code>description</code> 欄位是寫給<strong>未來自己</strong>的 recall trigger，不是寫給搜尋引擎的摘要。它要回答的問題是：「我在什麼情境下、遇到什麼問題，會需要回來讀這篇？」</p>
<p>類比：Claude Code skill 的 <code>description</code> 讓系統在對話中自動判斷「要不要載入這個 skill」。文章的 description 讓未來的自己在掃列表時自動判斷「要不要進去讀」。兩者目的相同——降低 recall 的認知成本。</p>
<p>摘要式 description 只回答「這篇<strong>在講什麼</strong>」（內容索引）；recall trigger 回答「<strong>你在什麼情境下需要這篇</strong>」（情境索引）。前者是被動的——搜尋命中後才看；後者是主動的——掃列表就能決定。</p>
<h2 id="情境">情境</h2>
<p>三篇 macOS 文章（新機設定 / 磁碟診斷 / App 佔用報告）的 description 寫法：</p>
<ul>
<li>原本：「從一台 30G 餘裕在幾小時內歸零的 Mac，記錄一套先看快照、再用實際佔用排查的磁碟診斷順序……」</li>
<li>問題：這是<strong>內容摘要</strong>——讀者已經知道要找「磁碟診斷」才會搜到它，description 只重述他已預期的內容，沒有增量資訊。</li>
<li>理想：description 應該告訴未來的自己「你什麼時候會需要回來」——例如「磁碟莫名滿載時的排查起手順序、避開 sparse 假大小陷阱、以及用 tmutil 判讀快照是否為元兇的方法」。</li>
</ul>
<h2 id="理想做法">理想做法</h2>
<p>description 撰寫時問自己三個問題：</p>
<ol>
<li><strong>我未來會在什麼情境下需要這篇？</strong>（觸發條件）</li>
<li><strong>這篇給我的關鍵判讀 / 操作是什麼？</strong>（帶走的能力）</li>
<li><strong>不讀這篇我會踩什麼坑？</strong>（省下的試錯）</li>
</ol>
<p>三者至少涵蓋一個。格式不重要，重要的是 description 讀完後能判斷「現在要不要進去」。</p>
<p>反例——以下句型通常是摘要不是 trigger：</p>
<ul>
<li>「記錄了 X 的過程」（日記式）</li>
<li>「介紹 X 的做法」（教科書式）</li>
<li>「從 X 事件整理出 Y」（報告式）</li>
</ul>
<p>這些句型把 description 當後設描述（meta-description of the article），而不是情境描述（description of when you need it）。</p>
<h3 id="寫法準則精準無假設無修辭">寫法準則：精準、無假設、無修辭</h3>
<p>description 給人判斷也給機器（AI）判斷，每個 token 都要有資訊量。三個不要：</p>
<ol>
<li><strong>不要操作細節</strong>——指令、參數、具體步驟留在內文。description 幫人決定「要不要點進去」，不是把文章壓縮塞進來。</li>
<li><strong>不要假設前提</strong>——「想不起來」「卻發現」「卻不確定」「不知從何下手」這些描述讀者的困境，不是資訊。直接說用途。</li>
<li><strong>不要情緒修辭</strong>——「別被騙」「打架」「空轉」「踩坑」這些不精準。用事實陳述取代。</li>
<li><strong>不要內嵌數字</strong>——「三項基礎建設」「6 段結構」「兩個陷阱」把成員數烤進 description，內文增刪就要同步改。description 描述文章的功能（解什麼問題），不描述文章的結構（幾段幾項）。理想的 description 無論內文怎麼調整都不用變動。同 <a href="../name-collections-by-role-not-count/">#156 集合命名不內嵌數量</a>。</li>
</ol>
<p>對比：</p>
<table>
  <thead>
      <tr>
          <th>有問題</th>
          <th>精準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>想不起 figure shortcode 還是 Markdown 時查這篇</td>
          <td>Hugo 文章插圖的寫法與路徑規則</td>
      </tr>
      <tr>
          <td>別被 fail 訊息指的 test 騙</td>
          <td>fail 位置看 <code>+N -1</code> 累計而非訊息標示的 test</td>
      </tr>
      <tr>
          <td>套模板每篇結構都打架時回來</td>
          <td>migration playbook 的結構選型流程</td>
      </tr>
      <tr>
          <td>三項基礎建設的依賴順序</td>
          <td>底層基礎建設的依賴順序（數字隨內容變會過時）</td>
      </tr>
  </tbody>
</table>
<p>150 字上限不是要塞滿的配額。短能到位就短。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li>列表頁的 description 變成一片「記錄了…」「整理出…」的重複句型，掃不出差異，每篇都要點進去才知道需不需要</li>
<li>日後同類情境再發生時，想不起來自己寫過、重新搜或重新踩坑</li>
<li>blog 的知識累積效益被 recall 成本吃掉——寫了等於沒寫</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>description 的主詞是「本文 / 這篇 / 記錄」→ 可能是摘要不是 trigger</li>
<li>description 刪掉後，只看 title 就能猜出 description 的全部內容 → 沒有增量</li>
<li>掃列表時無法在 3 秒內判斷「這篇跟我現在的問題有沒有關」→ trigger 失敗</li>
</ul>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<ul>
<li><a href="../atomic-note-needs-situational-entry/">#169 原子筆記要有向上的議題入口</a>：同根因——讀者（含未來的自己）需要「情境入口」而非「定義入口」。#169 談卡片正文的進入動機，本卡談 frontmatter description 的進入動機，是同一原則在不同 surface 的體現。</li>
<li><a href="../teaching-completeness-by-learner-journey/">#131 教材完整性要用讀者旅程驗證</a>：讀者旅程的第一站是列表頁的 description——旅程驗證如果從「已進入文章」開始，就跳過了「要不要進入」的判斷點。</li>
<li><a href="../audience-fork-before-jargon-wall/">#159 入口分流要放在詞彙牆之前</a>：description 是文章的入口分流欄位，分流依據應是讀者的情境而非文章的內容結構。</li>
</ul>
<hr>
]]></content:encoded></item><item><title>列舉與數字殘留在定義型文件會製造維護債務</title><link>https://tarrragon.github.io/blog/report/enumeration-creates-maintenance-debt/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/enumeration-creates-maintenance-debt/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>定義型文件（規則、規格、常駐名單——陳述「什麼成立」的文件，相對於報告、日誌這類記錄「當時發生什麼」的文件）中的&lt;strong>冗餘列舉和描述性數字&lt;/strong>是撰寫過程中的推理殘留——作者需要先計算範圍和數量來確認定義正確，但這個計算過程不該留在最終文件裡。判斷標準：&lt;strong>如果拿掉列舉或數字，讀者對定義的理解不受影響 → 刪除&lt;/strong>。&lt;/p>
&lt;p>下面兩個層次的殘留是觀察到的 pattern：&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>「所有情境（A-G）無條件加入」&lt;/td>
 &lt;td>新增情境 H 時要回頭更新所有引用&lt;/td>
 &lt;td>零——「所有情境」已完整表達&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>描述性數字&lt;/td>
 &lt;td>「9 個函式、428 行可獨立抽出」&lt;/td>
 &lt;td>任何函式增減都要校準計數&lt;/td>
 &lt;td>低——讀者需要的是哪些函式、不是幾個&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>列舉在定義型文件中也有合理用途：當列舉本身就是定義（封閉 enum、有限狀態機的狀態集、HTTP 方法集），列舉本身就是被定義的對象。區分方式：列舉是在&lt;strong>重述&lt;/strong>已由其他機制決定的範圍（冗餘）、還是在&lt;strong>建立&lt;/strong>範圍（定義）。&lt;/p>
&lt;p>同理，數字分兩種：&lt;strong>描述性數字&lt;/strong>報告現狀（「目前有 9 個函式」），程式碼一變就過時；&lt;strong>規範性數字&lt;/strong>規定目標值（「P99 &amp;lt; 200ms」「委員固定 7 人」「MAX_DEPTH=3」），是被定義的對象，該保留。&lt;/p>
&lt;hr>
&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡的論述基於 &lt;strong>2 個 case&lt;/strong>（parallel-evaluation SKILL.md 常駐委員定義 + W3-008 分析報告）抽出來的觀察，每個子論點（列舉 / 數字）各只有 1 個 case 支撐。具體限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>「列舉」指的是定義型文件中的冗餘列舉&lt;/strong>（如「所有情境（A-G）」），不包含報告格式範本中的 placeholder（如「情境: [A-G]」）——後者是填空欄位、不是定義；也不包含本身就是定義的封閉列舉（如 enum 成員清單）&lt;/li>
&lt;li>&lt;strong>「數字」指的是描述性計數&lt;/strong>（如「9 個函式」「428 行」），不包含規範性數字（如「MAX_DEPTH=3」「P99 &amp;lt; 200ms」）——後者是設計決策或 SLA 目標、不是統計快照&lt;/li>
&lt;li>&lt;strong>推理殘留不限於 AI 寫作&lt;/strong>：兩個 case 都來自 AI 生成的文件、AI 的生成順序可能讓這類殘留更常出現（見下方觀察），但人類作者在撰寫定義時同樣會先列舉確認再寫結論、忘了刪除中間產物&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="觸發案例">觸發案例&lt;/h2>
&lt;h3 id="case-1常駐委員定義的冗餘列舉">Case 1：常駐委員定義的冗餘列舉&lt;/h3>
&lt;p>parallel-evaluation SKILL.md 常駐委員表格寫了「所有情境（A-G）無條件加入」。「所有情境」已是完整定義，「（A-G）」在語意上重複——拿掉後讀者理解不受影響。留在文件裡則變成維護債務：新增情境 H 時「（A-G）」過時，讀者可能誤判 H 不含。&lt;/p>
&lt;p>&lt;strong>修正&lt;/strong>：刪除「（A-G）」。「所有情境無條件加入」自動涵蓋未來新增的情境。&lt;/p>
&lt;h3 id="case-2分析報告的數字快照">Case 2：分析報告的數字快照&lt;/h3>
&lt;p>W3-008 分析報告寫了「duplicate_detection 群組 9 個函式、428 行」。這是分析時的統計快照、作為報告紀錄合理。但如果這些數字被寫進 ticket 驗收條件（「duplicate_detector.py 含 9 個函式」「create.py 行數減少約 428 行」），重構過程中任何函式增減都會讓驗收條件失效。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：分析報告中的數字 → 保留（紀錄）。驗收條件中的描述性計數 → 改用結構性斷言（「所有 duplicate 相關函式已移至新模組」「既有測試通過」）。&lt;/p>
&lt;hr>
&lt;h2 id="ai-生成的順序觀察">AI 生成的順序觀察&lt;/h2>
&lt;p>兩個 case 都來自 AI 生成的文件。AI 的生成順序是&lt;strong>先推理 → 再生成文字&lt;/strong>——模型在寫「所有情境無條件加入」之前，內部已驗證了 A 到 G 都涵蓋，列舉是推理的副產物被寫進生成的文字裡。同理，AI 在寫函式列表時傾向先數數量再列內容（「9 個函式：_tokenize, &amp;hellip;」），數量是為了自我驗證完整性，但讀者只需要函式列表本身。&lt;/p>
&lt;p>人類作者在撰寫定義時也會先列舉確認涵蓋範圍、AI 是否讓此模式更頻繁出現仍待更多樣本驗證（見限制段）。無論成因，判準相同：拿掉後讀者理解不受影響 → 刪除。&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>定義型（規則、規格、常駐名單）&lt;/td>
 &lt;td>推理殘留&lt;/td>
 &lt;td>刪除——定義本身已完整&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>報告型（分析、日誌、檢討）&lt;/td>
 &lt;td>紀錄快照&lt;/td>
 &lt;td>保留——讀者需要知道當時的狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗收條件&lt;/td>
 &lt;td>依被測屬性判斷&lt;/td>
 &lt;td>穩定屬性（函式名稱清單）→ 保留；易變屬性（行數、計數）→ 改結構性斷言&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範本 placeholder&lt;/td>
 &lt;td>填空欄位&lt;/td>
 &lt;td>保留——提示使用者填入具體值&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>驗收條件的判斷依據是&lt;strong>被測屬性是否隨重構變動&lt;/strong>。函式名稱清單在重構後仍可驗證（名稱沒變、只是搬家），但行數和函式計數會因任何增減而失效——改用結構性斷言（「所有 X 相關函式已移至新模組」）讓驗收條件跟重構過程解耦。&lt;/p>
&lt;p>真實文件常混合定義段與分析段（如 SKILL.md 同時有規則定義和設計紀錄），分類單位是&lt;strong>段落&lt;/strong>而非整份文件——同一份文件內的定義段套用刪除判準、分析段保留數字。範本 placeholder 同理，它是填空欄位、不是定義。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他原則的關係">跟其他原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/reader-does-not-need-to-know/" data-link-title="讀者不需要知道 — 刪除比解釋更尊重讀者" data-link-desc="「整理目的」blockquote 告訴讀者這篇文章的寫作動機和邊界。但讀者不需要知道作者為什麼寫這篇文章——他們需要知道讀完能帶走什麼。meta 資訊（寫作動機、邊界聲明、脈絡解釋）服務的是作者的組織需求，不是讀者的閱讀需求。">讀者不需要知道的資訊不該出現在最終文件&lt;/a> — 同根原則的兩種表現：該卡處理 meta 動機殘留（「為什麼寫這篇」）、本卡處理推理過程殘留（列舉和數字）。兩者共用「拿掉後讀者體驗變不變差」的判準&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/applicability-scope-must-be-enumerated/" data-link-title="適用範圍要展開成 file enumeration、口語描述不夠" data-link-desc="原則的『適用範圍』寫成口語描述（『所有教學文件的論證段落』）時、執行 review 的人要當場推導『具體哪些檔屬於這個範圍』、推導步驟容易漏；改寫成 enumerated file list（具體列出 file paths）就能避免 enumeration 不完整。Enumerate 的合法形式是『可被 grep / find 重現的具體 file 集合』、不是『口語類型描述』。本卡是 #95 的下游具體化、跟 #82 互補：enumerate 是字面層、enumeration completeness 是行為層判準。">適用範圍要展開成 file enumeration&lt;/a> — 鏡像關係：該卡主張「適用範圍要 enumerate」、本卡主張「定義型文件的冗餘列舉要刪」。取決於文件型別——scope 文件需要 enumeration 確保執行時不遺漏、定義型文件的範圍已由定義本身表達&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">集合命名用角色、不內嵌數量&lt;/a> — 同原則在不同層面：前者處理命名層（「六大原則」的數字是成員清單的衍生值）、本卡處理正文定義層（「9 個函式」的數字是程式碼的衍生值）。兩卡共享「外部凍結數字可留」的邊界（前者的 SOLID / OWASP 對應本卡的 SLA / 設計常數）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">便利驅動的寫法會偏離意圖&lt;/a> — 上位原則：列舉殘留是「寫的時候方便（先算再寫）、讀的時候多餘」的具體實例&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p>定義型文件（規則、規格、常駐名單——陳述「什麼成立」的文件，相對於報告、日誌這類記錄「當時發生什麼」的文件）中的<strong>冗餘列舉和描述性數字</strong>是撰寫過程中的推理殘留——作者需要先計算範圍和數量來確認定義正確，但這個計算過程不該留在最終文件裡。判斷標準：<strong>如果拿掉列舉或數字，讀者對定義的理解不受影響 → 刪除</strong>。</p>
<p>下面兩個層次的殘留是觀察到的 pattern：</p>
<table>
  <thead>
      <tr>
          <th>殘留類型</th>
          <th>文件中的樣子</th>
          <th>維護成本</th>
          <th>閱讀價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>冗餘列舉</td>
          <td>「所有情境（A-G）無條件加入」</td>
          <td>新增情境 H 時要回頭更新所有引用</td>
          <td>零——「所有情境」已完整表達</td>
      </tr>
      <tr>
          <td>描述性數字</td>
          <td>「9 個函式、428 行可獨立抽出」</td>
          <td>任何函式增減都要校準計數</td>
          <td>低——讀者需要的是哪些函式、不是幾個</td>
      </tr>
  </tbody>
</table>
<p>列舉在定義型文件中也有合理用途：當列舉本身就是定義（封閉 enum、有限狀態機的狀態集、HTTP 方法集），列舉本身就是被定義的對象。區分方式：列舉是在<strong>重述</strong>已由其他機制決定的範圍（冗餘）、還是在<strong>建立</strong>範圍（定義）。</p>
<p>同理，數字分兩種：<strong>描述性數字</strong>報告現狀（「目前有 9 個函式」），程式碼一變就過時；<strong>規範性數字</strong>規定目標值（「P99 &lt; 200ms」「委員固定 7 人」「MAX_DEPTH=3」），是被定義的對象，該保留。</p>
<hr>
<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡的論述基於 <strong>2 個 case</strong>（parallel-evaluation SKILL.md 常駐委員定義 + W3-008 分析報告）抽出來的觀察，每個子論點（列舉 / 數字）各只有 1 個 case 支撐。具體限制：</p>
<ul>
<li><strong>「列舉」指的是定義型文件中的冗餘列舉</strong>（如「所有情境（A-G）」），不包含報告格式範本中的 placeholder（如「情境: [A-G]」）——後者是填空欄位、不是定義；也不包含本身就是定義的封閉列舉（如 enum 成員清單）</li>
<li><strong>「數字」指的是描述性計數</strong>（如「9 個函式」「428 行」），不包含規範性數字（如「MAX_DEPTH=3」「P99 &lt; 200ms」）——後者是設計決策或 SLA 目標、不是統計快照</li>
<li><strong>推理殘留不限於 AI 寫作</strong>：兩個 case 都來自 AI 生成的文件、AI 的生成順序可能讓這類殘留更常出現（見下方觀察），但人類作者在撰寫定義時同樣會先列舉確認再寫結論、忘了刪除中間產物</li>
</ul>
<hr>
<h2 id="觸發案例">觸發案例</h2>
<h3 id="case-1常駐委員定義的冗餘列舉">Case 1：常駐委員定義的冗餘列舉</h3>
<p>parallel-evaluation SKILL.md 常駐委員表格寫了「所有情境（A-G）無條件加入」。「所有情境」已是完整定義，「（A-G）」在語意上重複——拿掉後讀者理解不受影響。留在文件裡則變成維護債務：新增情境 H 時「（A-G）」過時，讀者可能誤判 H 不含。</p>
<p><strong>修正</strong>：刪除「（A-G）」。「所有情境無條件加入」自動涵蓋未來新增的情境。</p>
<h3 id="case-2分析報告的數字快照">Case 2：分析報告的數字快照</h3>
<p>W3-008 分析報告寫了「duplicate_detection 群組 9 個函式、428 行」。這是分析時的統計快照、作為報告紀錄合理。但如果這些數字被寫進 ticket 驗收條件（「duplicate_detector.py 含 9 個函式」「create.py 行數減少約 428 行」），重構過程中任何函式增減都會讓驗收條件失效。</p>
<p><strong>判讀</strong>：分析報告中的數字 → 保留（紀錄）。驗收條件中的描述性計數 → 改用結構性斷言（「所有 duplicate 相關函式已移至新模組」「既有測試通過」）。</p>
<hr>
<h2 id="ai-生成的順序觀察">AI 生成的順序觀察</h2>
<p>兩個 case 都來自 AI 生成的文件。AI 的生成順序是<strong>先推理 → 再生成文字</strong>——模型在寫「所有情境無條件加入」之前，內部已驗證了 A 到 G 都涵蓋，列舉是推理的副產物被寫進生成的文字裡。同理，AI 在寫函式列表時傾向先數數量再列內容（「9 個函式：_tokenize, &hellip;」），數量是為了自我驗證完整性，但讀者只需要函式列表本身。</p>
<p>人類作者在撰寫定義時也會先列舉確認涵蓋範圍、AI 是否讓此模式更頻繁出現仍待更多樣本驗證（見限制段）。無論成因，判準相同：拿掉後讀者理解不受影響 → 刪除。</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>報告型（分析、日誌、檢討）</td>
          <td>紀錄快照</td>
          <td>保留——讀者需要知道當時的狀態</td>
      </tr>
      <tr>
          <td>驗收條件</td>
          <td>依被測屬性判斷</td>
          <td>穩定屬性（函式名稱清單）→ 保留；易變屬性（行數、計數）→ 改結構性斷言</td>
      </tr>
      <tr>
          <td>範本 placeholder</td>
          <td>填空欄位</td>
          <td>保留——提示使用者填入具體值</td>
      </tr>
  </tbody>
</table>
<p>驗收條件的判斷依據是<strong>被測屬性是否隨重構變動</strong>。函式名稱清單在重構後仍可驗證（名稱沒變、只是搬家），但行數和函式計數會因任何增減而失效——改用結構性斷言（「所有 X 相關函式已移至新模組」）讓驗收條件跟重構過程解耦。</p>
<p>真實文件常混合定義段與分析段（如 SKILL.md 同時有規則定義和設計紀錄），分類單位是<strong>段落</strong>而非整份文件——同一份文件內的定義段套用刪除判準、分析段保留數字。範本 placeholder 同理，它是填空欄位、不是定義。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<ul>
<li><a href="/blog/report/reader-does-not-need-to-know/" data-link-title="讀者不需要知道 — 刪除比解釋更尊重讀者" data-link-desc="「整理目的」blockquote 告訴讀者這篇文章的寫作動機和邊界。但讀者不需要知道作者為什麼寫這篇文章——他們需要知道讀完能帶走什麼。meta 資訊（寫作動機、邊界聲明、脈絡解釋）服務的是作者的組織需求，不是讀者的閱讀需求。">讀者不需要知道的資訊不該出現在最終文件</a> — 同根原則的兩種表現：該卡處理 meta 動機殘留（「為什麼寫這篇」）、本卡處理推理過程殘留（列舉和數字）。兩者共用「拿掉後讀者體驗變不變差」的判準</li>
<li><a href="/blog/report/applicability-scope-must-be-enumerated/" data-link-title="適用範圍要展開成 file enumeration、口語描述不夠" data-link-desc="原則的『適用範圍』寫成口語描述（『所有教學文件的論證段落』）時、執行 review 的人要當場推導『具體哪些檔屬於這個範圍』、推導步驟容易漏；改寫成 enumerated file list（具體列出 file paths）就能避免 enumeration 不完整。Enumerate 的合法形式是『可被 grep / find 重現的具體 file 集合』、不是『口語類型描述』。本卡是 #95 的下游具體化、跟 #82 互補：enumerate 是字面層、enumeration completeness 是行為層判準。">適用範圍要展開成 file enumeration</a> — 鏡像關係：該卡主張「適用範圍要 enumerate」、本卡主張「定義型文件的冗餘列舉要刪」。取決於文件型別——scope 文件需要 enumeration 確保執行時不遺漏、定義型文件的範圍已由定義本身表達</li>
<li><a href="/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">集合命名用角色、不內嵌數量</a> — 同原則在不同層面：前者處理命名層（「六大原則」的數字是成員清單的衍生值）、本卡處理正文定義層（「9 個函式」的數字是程式碼的衍生值）。兩卡共享「外部凍結數字可留」的邊界（前者的 SOLID / OWASP 對應本卡的 SLA / 設計常數）</li>
<li><a href="/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">便利驅動的寫法會偏離意圖</a> — 上位原則：列舉殘留是「寫的時候方便（先算再寫）、讀的時候多餘」的具體實例</li>
</ul>
]]></content:encoded></item><item><title>讀者是缺經驗的專業人士、不是外行人</title><link>https://tarrragon.github.io/blog/report/audience-is-professional-not-layperson/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/audience-is-professional-not-layperson/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 infra 教學模組的寫作 retrospective。21 篇文章初稿完成後，入門層文章（「個人專案到團隊服務」「一台機器到三個環境」）和溝通層文章（「給非工程人員的 infra 說明」）採用了宣導式框架（故事線導入、辦公室比喻、「跑得好好的」語氣），經作者 review 判定不適合目標讀者，重寫為「補足專業人士經驗缺口」的框架。限制：evidence 來自單一教學模組的一次重寫週期，跨領域適用性未驗證。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>技術教材的讀者是在特定領域缺乏經驗的專業人士，不是完全不懂的外行人。他們有系統思考能力、理解風險與成本、能處理抽象概念，只是沒有經歷過這個領域的具體情境。&lt;/p>
&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>讀者不懂、需要被說服&lt;/td>
 &lt;td>降低可信度 — 專業人士感到被低估&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>經驗補足&lt;/td>
 &lt;td>讀者有能力判斷、缺的是情境&lt;/td>
 &lt;td>建立信任 — 讀者在熟悉的認知框架裡接收新資訊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>infra 教學模組的入門層文章原本採用宣導式框架：&lt;/p>
&lt;ul>
&lt;li>「一台機器跑得好好的」— 預設讀者沒想過為什麼需要多環境&lt;/li>
&lt;li>「個人專案到團隊服務：infra 在哪裡出現」— 用 side project 故事引導讀者「發現」infra&lt;/li>
&lt;li>「用辦公室理解 infra」— 用辦公大樓、門禁卡、資產清冊比喻 VPC、IAM、state&lt;/li>
&lt;/ul>
&lt;p>這些框架的問題是它們預設讀者既不知道 infra 存在、也無法理解技術概念。但教材的目標讀者是專業工程師（缺 infra 經驗但有系統思考能力）和專業決策者（缺技術細節但懂營運風險）。宣導式語氣對這兩類讀者都是失配。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;h3 id="對工程師讀者">對工程師讀者&lt;/h3>
&lt;p>直接描述情境和操作需求，不用故事線包裝。不說「跑得好好的、直到有一天…」，而說「單機環境在需要測試環境時會遇到三個操作問題」。讀者有能力從情境描述中自行判斷這跟自己的關聯。&lt;/p>
&lt;p>每個概念用「它解什麼問題、不管理時的後果」框架帶入，不用「你可能沒注意到，其實你已經在用…」的發現式框架。後者暗示讀者對自己的工作環境不熟悉。&lt;/p>
&lt;h3 id="對非工程決策者">對非工程決策者&lt;/h3>
&lt;p>減少專業術語、讓討論的情境從簡單到複雜遞進，而非大量使用比喻。比喻的問題是它只傳遞形狀、不傳遞嚴重性 — 「VPC 像辦公大樓」讓人理解隔離的概念，但不讓人理解隔離失敗時的商業代價。從管理層面解釋「會發生什麼事、為什麼要避免、發生了怎麼處理」比從技術層面解釋「這個元件是什麼」有效。&lt;/p>
&lt;p>決策者需要的資訊是：這個風險的量級（影響多少客戶、停多久、多少錢）、補救的成本曲線（現在做 vs 半年後做的差距）、以及投入的時間框架。這些都是他們熟悉的決策維度，用這些維度講就好。&lt;/p>
&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>：讀者記住了「VPC 像辦公大樓」，但當他需要判斷 CIDR 該切多大、需不需要跟其他 VPC peering 時，比喻反而成為思考的障礙 — 辦公大樓沒有地址空間的概念。&lt;/li>
&lt;li>&lt;strong>推動失效&lt;/strong>：infra 推不動的原因是「提案語言跟決策者語言對不上」，不是「決策者太笨聽不懂」。把溝通問題歸因成理解力問題，會導致越比喻越多、越比喻越離題。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>寫教材時如果發現自己在用以下句型，代表可能掉進了宣導式框架：&lt;/p>
&lt;ul>
&lt;li>「你可能沒注意到…」「你可能不知道…」— 預設讀者無知&lt;/li>
&lt;li>「想像一下…」「把 X 想成 Y…」— 用比喻替代直接說明&lt;/li>
&lt;li>「跑得好好的」「聽起來很複雜」— 用語氣管理讀者情緒&lt;/li>
&lt;li>「其實很簡單」「說穿了就是」— 降低複雜度的假象&lt;/li>
&lt;/ul>
&lt;p>替代方式：直接描述情境、列出操作需求、說明不做的後果。讀者有能力從事實中得出判斷。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/register-violation-needs-cross-style-eyes/" data-link-title="register 違規：偵測可機械化、判定要靠文體異源的眼睛" data-link-desc="寫作規範的違規分兩類：形式違規（emoji / 編號 / broken link）可完全機械判定、該進工具鏈；register / 品味違規（概念前置 / 否定起手 / 喊話 / 誇飾）的判定有不可消除的品味核心。「不是 X、而是 Y」的陷阱是偵測可機械化（grep 抓得到句型）偽裝成判定可機械化、誘導無限投入更精緻的判定方法（grep → 概念位置 → 行為測試）、但判定始終在品味側、始終放水。更深一層：產出這類違規的 LLM 跟審查它的 LLM 共享文體直覺、同源自審對 register 違規有結構上限、加再多輪次都跨不過。結構解是引入文體異源的視角（人類冷讀 / reader-simulation / 對抗文體 reviewer）、並接受 100% 自動 catch 不可能。">#165 register 違規需要跨文體視角&lt;/a>：宣導語氣是一種 register 問題 — LLM 自然傾向「對讀者友善」的語氣，自審時很難察覺它已經偏向宣導&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/lead-with-the-point-cross-language/" data-link-title="重點優先陳述是跨語言的資訊結構原則、不是中文句型問題" data-link-desc="正向陳述優先的本質是資訊結構效率：讀者拿到核心概念的認知步驟越少越好。「不是 X、而是 Y」表達能力差、是因為它讓讀者先處理一個被否定的錯誤理解 X 才拿到正確的 Y、重點後置多繞一步。這個缺陷跨語言成立——英文 not X but Y、日文 X ではなく Y 同樣高頻、換語言不打破（證偽過的反例假設）。判別線是「核心概念在不在最前」、統一了 #94（重點先行合法）與 #149（重點後置違規）、且可操作。LLM 系統性放水的根因是高頻偏置（把語料高頻句型評為表達好、高頻不等於資訊結構優、跨語言）。主解是強制執行重點位置判準、#165 的異源視角降為補充。">#166 重點先行是跨語言的資訊結構&lt;/a>：宣導式框架常把重點藏在故事線之後（「跑得好好的…直到…」），經驗補足式直接把操作需求放句首&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">教材用中性陳述、不對讀者喊話&lt;/a>：宣導語氣跟喊話是相鄰問題 — 都源自「讀者需要被管理」的隱含假設&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 infra 教學模組的寫作 retrospective。21 篇文章初稿完成後，入門層文章（「個人專案到團隊服務」「一台機器到三個環境」）和溝通層文章（「給非工程人員的 infra 說明」）採用了宣導式框架（故事線導入、辦公室比喻、「跑得好好的」語氣），經作者 review 判定不適合目標讀者，重寫為「補足專業人士經驗缺口」的框架。限制：evidence 來自單一教學模組的一次重寫週期，跨領域適用性未驗證。</p>
<h2 id="核心原則">核心原則</h2>
<p>技術教材的讀者是在特定領域缺乏經驗的專業人士，不是完全不懂的外行人。他們有系統思考能力、理解風險與成本、能處理抽象概念，只是沒有經歷過這個領域的具體情境。</p>
<p>寫法的差別在於進入方式：</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>建立信任 — 讀者在熟悉的認知框架裡接收新資訊</td>
      </tr>
  </tbody>
</table>
<h2 id="情境">情境</h2>
<p>infra 教學模組的入門層文章原本採用宣導式框架：</p>
<ul>
<li>「一台機器跑得好好的」— 預設讀者沒想過為什麼需要多環境</li>
<li>「個人專案到團隊服務：infra 在哪裡出現」— 用 side project 故事引導讀者「發現」infra</li>
<li>「用辦公室理解 infra」— 用辦公大樓、門禁卡、資產清冊比喻 VPC、IAM、state</li>
</ul>
<p>這些框架的問題是它們預設讀者既不知道 infra 存在、也無法理解技術概念。但教材的目標讀者是專業工程師（缺 infra 經驗但有系統思考能力）和專業決策者（缺技術細節但懂營運風險）。宣導式語氣對這兩類讀者都是失配。</p>
<h2 id="理想做法">理想做法</h2>
<h3 id="對工程師讀者">對工程師讀者</h3>
<p>直接描述情境和操作需求，不用故事線包裝。不說「跑得好好的、直到有一天…」，而說「單機環境在需要測試環境時會遇到三個操作問題」。讀者有能力從情境描述中自行判斷這跟自己的關聯。</p>
<p>每個概念用「它解什麼問題、不管理時的後果」框架帶入，不用「你可能沒注意到，其實你已經在用…」的發現式框架。後者暗示讀者對自己的工作環境不熟悉。</p>
<h3 id="對非工程決策者">對非工程決策者</h3>
<p>減少專業術語、讓討論的情境從簡單到複雜遞進，而非大量使用比喻。比喻的問題是它只傳遞形狀、不傳遞嚴重性 — 「VPC 像辦公大樓」讓人理解隔離的概念，但不讓人理解隔離失敗時的商業代價。從管理層面解釋「會發生什麼事、為什麼要避免、發生了怎麼處理」比從技術層面解釋「這個元件是什麼」有效。</p>
<p>決策者需要的資訊是：這個風險的量級（影響多少客戶、停多久、多少錢）、補救的成本曲線（現在做 vs 半年後做的差距）、以及投入的時間框架。這些都是他們熟悉的決策維度，用這些維度講就好。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<p>宣導式寫法的具體代價：</p>
<ul>
<li><strong>可信度損失</strong>：工程師讀到「你可能不知道」時，如果他其實知道（只是沒做過），教材在他心中的權威性立刻下降。一旦開頭失去信任，後面的技術判準也會被打折。</li>
<li><strong>比喻阻礙深度理解</strong>：讀者記住了「VPC 像辦公大樓」，但當他需要判斷 CIDR 該切多大、需不需要跟其他 VPC peering 時，比喻反而成為思考的障礙 — 辦公大樓沒有地址空間的概念。</li>
<li><strong>推動失效</strong>：infra 推不動的原因是「提案語言跟決策者語言對不上」，不是「決策者太笨聽不懂」。把溝通問題歸因成理解力問題，會導致越比喻越多、越比喻越離題。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>寫教材時如果發現自己在用以下句型，代表可能掉進了宣導式框架：</p>
<ul>
<li>「你可能沒注意到…」「你可能不知道…」— 預設讀者無知</li>
<li>「想像一下…」「把 X 想成 Y…」— 用比喻替代直接說明</li>
<li>「跑得好好的」「聽起來很複雜」— 用語氣管理讀者情緒</li>
<li>「其實很簡單」「說穿了就是」— 降低複雜度的假象</li>
</ul>
<p>替代方式：直接描述情境、列出操作需求、說明不做的後果。讀者有能力從事實中得出判斷。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/register-violation-needs-cross-style-eyes/" data-link-title="register 違規：偵測可機械化、判定要靠文體異源的眼睛" data-link-desc="寫作規範的違規分兩類：形式違規（emoji / 編號 / broken link）可完全機械判定、該進工具鏈；register / 品味違規（概念前置 / 否定起手 / 喊話 / 誇飾）的判定有不可消除的品味核心。「不是 X、而是 Y」的陷阱是偵測可機械化（grep 抓得到句型）偽裝成判定可機械化、誘導無限投入更精緻的判定方法（grep → 概念位置 → 行為測試）、但判定始終在品味側、始終放水。更深一層：產出這類違規的 LLM 跟審查它的 LLM 共享文體直覺、同源自審對 register 違規有結構上限、加再多輪次都跨不過。結構解是引入文體異源的視角（人類冷讀 / reader-simulation / 對抗文體 reviewer）、並接受 100% 自動 catch 不可能。">#165 register 違規需要跨文體視角</a>：宣導語氣是一種 register 問題 — LLM 自然傾向「對讀者友善」的語氣，自審時很難察覺它已經偏向宣導</li>
<li>→ <a href="/blog/report/lead-with-the-point-cross-language/" data-link-title="重點優先陳述是跨語言的資訊結構原則、不是中文句型問題" data-link-desc="正向陳述優先的本質是資訊結構效率：讀者拿到核心概念的認知步驟越少越好。「不是 X、而是 Y」表達能力差、是因為它讓讀者先處理一個被否定的錯誤理解 X 才拿到正確的 Y、重點後置多繞一步。這個缺陷跨語言成立——英文 not X but Y、日文 X ではなく Y 同樣高頻、換語言不打破（證偽過的反例假設）。判別線是「核心概念在不在最前」、統一了 #94（重點先行合法）與 #149（重點後置違規）、且可操作。LLM 系統性放水的根因是高頻偏置（把語料高頻句型評為表達好、高頻不等於資訊結構優、跨語言）。主解是強制執行重點位置判準、#165 的異源視角降為補充。">#166 重點先行是跨語言的資訊結構</a>：宣導式框架常把重點藏在故事線之後（「跑得好好的…直到…」），經驗補足式直接把操作需求放句首</li>
<li>→ <a href="/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">教材用中性陳述、不對讀者喊話</a>：宣導語氣跟喊話是相鄰問題 — 都源自「讀者需要被管理」的隱含假設</li>
</ul>
]]></content:encoded></item><item><title>跨專業溝通用情境遞進、不用比喻堆疊</title><link>https://tarrragon.github.io/blog/report/cross-expertise-communication-scenario-not-analogy/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cross-expertise-communication-scenario-not-analogy/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 infra 教學模組的溝通層文章重寫。原稿用辦公大樓比喻解釋 VPC / IAM / IaC 等概念給非工程決策者，經作者 review 判定比喻程度過重、反而阻礙溝通。重寫後改為管理視角的情境遞進。限制：evidence 來自單一模組的一次重寫，跨領域（醫療→管理、法律→工程等）的適用性未驗證。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>向非本領域的專業人士解釋技術議題時，有效的做法是減少術語、讓情境從簡單到複雜遞進。堆疊比喻（「VPC 像辦公大樓」「IAM 像門禁卡」「state 像資產清冊」）看似降低門檻，但有三個結構性問題。&lt;/p>
&lt;p>第一，比喻傳遞形狀但不傳遞嚴重性。「VPC 像辦公大樓的圍牆」讓人理解隔離這個概念，但不讓人理解隔離失敗時一個被入侵的服務能橫向存取所有資料庫的商業代價。決策者需要的是後者。&lt;/p>
&lt;p>第二，比喻在細節處崩解。辦公大樓沒有 CIDR 地址空間、沒有 peering、沒有跨區冗餘。當討論深入到需要判斷「CIDR 該切多大」或「要不要跨帳號」時，比喻不但幫不上忙，還會成為錯誤直覺的來源 — 讀者開始用辦公大樓的邏輯推斷網路架構。&lt;/p>
&lt;p>第三，過度比喻隱含「對方聽不懂」的預設。專業決策者能聽懂「環境描述檔讓系統可以在故障後重建」，不需要先繞到「就像建築藍圖」才能理解。多一層比喻是多一層認知負擔，不是少一層。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>infra 教材的溝通層文章原稿用延伸比喻架構整篇文章：&lt;/p>
&lt;ul>
&lt;li>VPC = 辦公大樓，有自己的地址範圍&lt;/li>
&lt;li>Subnet = 樓層分區（public = 大廳，private = 辦公區）&lt;/li>
&lt;li>Security group = 每個房間的門禁卡設定&lt;/li>
&lt;li>IAM = 員工證與權限&lt;/li>
&lt;li>IaC = 建築藍圖&lt;/li>
&lt;li>State = 資產清冊&lt;/li>
&lt;/ul>
&lt;p>每個比喻單獨看都成立，但串在一起時文章變成了一個辦公室管理指南，讀者要在「比喻世界」和「技術世界」之間反覆切換。決策者讀完後記住的是比喻、不是操作風險。&lt;/p>
&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>對應的 infra 能力&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>服務掛了，沒人知道怎麼重建&lt;/td>
 &lt;td>環境描述檔（IaC）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>重建了，但不確定跟之前一樣&lt;/td>
 &lt;td>state 追蹤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>測試時的操作打到了正式客戶&lt;/td>
 &lt;td>環境分離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>不知道誰有權限存取什麼&lt;/td>
 &lt;td>身分與權限管控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>出事了，查不到是誰在什麼時候改了什麼&lt;/td>
 &lt;td>變更紀錄&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每一行用決策者熟悉的語言：影響範圍（幾個客戶）、恢復時間（幾小時 vs 幾天）、成本（工程師時間 + 停機損失）。這些維度是決策者日常在處理的，不需要翻譯。&lt;/p>
&lt;p>情境遞進的優勢是它讓對方在自己的認知框架裡接收新資訊 — 管理者懂「服務掛了要能重建」，從這個已知出發、往「怎麼確保重建結果一致」「怎麼避免測試影響正式」遞進，每一步都建立在前一步的理解上。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>記住比喻、忘記風險&lt;/strong>：決策者開會時說「那個門禁卡的東西做了嗎」，但說不出它解決什麼問題、不做會怎樣。比喻成了一個替代理解的標籤。&lt;/li>
&lt;li>&lt;strong>比喻限制討論深度&lt;/strong>：當需要討論「要不要把 production 拆到獨立帳號」時，辦公大樓比喻沒有對應物。討論被迫回到技術語言，之前的比喻投資歸零。&lt;/li>
&lt;li>&lt;strong>溝通問題被錯誤歸因&lt;/strong>：推不動 infra 時，團隊以為是「沒解釋清楚」，於是加更多比喻。但問題其實是提案語言跟決策語言的框架不對，不是理解力不足。結果越比喻越偏離決策者的關注點。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>寫跨專業溝通文件時，如果發現自己在做以下事情，代表可能掉進了比喻堆疊：&lt;/p>
&lt;ul>
&lt;li>為每個技術概念都找一個日常比喻（「X 就像 Y」超過三個）&lt;/li>
&lt;li>比喻之間需要互相引用才能成立（「記得剛才說的大樓嗎？這個是大樓裡的…」）&lt;/li>
&lt;li>比喻崩解後要補救（「辦公大樓的比喻在這裡不完全適用，但大致上…」）&lt;/li>
&lt;li>文章的主要結構是比喻而非情境（H2 標題是比喻物、不是管理問題）&lt;/li>
&lt;/ul>
&lt;p>替代方式：每個概念用「它解什麼管理問題 + 不做的後果量級」一句話帶入，需要深入時用情境遞進展開。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人&lt;/a>：本卡的前提 — 跨專業溝通的對象是專業人士，比喻堆疊預設對方「聽不懂」&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">#155 用語意標題引用、不用位置編號&lt;/a>：情境遞進跟語意引用同源 — 都是用內容本身（情境描述 / 語意標題）承載意義，不靠外部代理物（比喻 / 編號）&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/cross-round-review-stopping-signal/" data-link-title="跨輪 review 停止訊號是 frame 涵蓋、不是 finding 數遞減" data-link-desc="判斷「該不該再來一輪 review」的訊號是『frame 軸是否還有未動』、不是『上一輪 finding 變少』；多輪 review 的 ROI 不是 monotonically decreasing、而是 frame 切換的質性轉換 — Round N 用新 frame 通常仍會抓出 substantial finding、但內容從 surface compliance 往深層 structural issue 走；停止訊號是「下一輪可用的新 frame 已經想不出來」、不是 finding 數遞減；本卡填補 #114 / #126 / #147 沒覆蓋的「何時夠了」判讀缺口">#148 跨輪 review 停止訊號&lt;/a>：溝通層文章的 review 停止訊號之一是「比喻已經從輔助變成主體」— 這個 frame 在 Round 3 steelman 可以 catch&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 infra 教學模組的溝通層文章重寫。原稿用辦公大樓比喻解釋 VPC / IAM / IaC 等概念給非工程決策者，經作者 review 判定比喻程度過重、反而阻礙溝通。重寫後改為管理視角的情境遞進。限制：evidence 來自單一模組的一次重寫，跨領域（醫療→管理、法律→工程等）的適用性未驗證。</p>
<h2 id="核心原則">核心原則</h2>
<p>向非本領域的專業人士解釋技術議題時，有效的做法是減少術語、讓情境從簡單到複雜遞進。堆疊比喻（「VPC 像辦公大樓」「IAM 像門禁卡」「state 像資產清冊」）看似降低門檻，但有三個結構性問題。</p>
<p>第一，比喻傳遞形狀但不傳遞嚴重性。「VPC 像辦公大樓的圍牆」讓人理解隔離這個概念，但不讓人理解隔離失敗時一個被入侵的服務能橫向存取所有資料庫的商業代價。決策者需要的是後者。</p>
<p>第二，比喻在細節處崩解。辦公大樓沒有 CIDR 地址空間、沒有 peering、沒有跨區冗餘。當討論深入到需要判斷「CIDR 該切多大」或「要不要跨帳號」時，比喻不但幫不上忙，還會成為錯誤直覺的來源 — 讀者開始用辦公大樓的邏輯推斷網路架構。</p>
<p>第三，過度比喻隱含「對方聽不懂」的預設。專業決策者能聽懂「環境描述檔讓系統可以在故障後重建」，不需要先繞到「就像建築藍圖」才能理解。多一層比喻是多一層認知負擔，不是少一層。</p>
<h2 id="情境">情境</h2>
<p>infra 教材的溝通層文章原稿用延伸比喻架構整篇文章：</p>
<ul>
<li>VPC = 辦公大樓，有自己的地址範圍</li>
<li>Subnet = 樓層分區（public = 大廳，private = 辦公區）</li>
<li>Security group = 每個房間的門禁卡設定</li>
<li>IAM = 員工證與權限</li>
<li>IaC = 建築藍圖</li>
<li>State = 資產清冊</li>
</ul>
<p>每個比喻單獨看都成立，但串在一起時文章變成了一個辦公室管理指南，讀者要在「比喻世界」和「技術世界」之間反覆切換。決策者讀完後記住的是比喻、不是操作風險。</p>
<h2 id="理想做法">理想做法</h2>
<p>用情境遞進替代比喻堆疊。每個技術概念對應到它解決的管理問題，然後從最簡單的情境開始、逐步加入複雜度：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>情境</th>
          <th>對應的 infra 能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>服務掛了，沒人知道怎麼重建</td>
          <td>環境描述檔（IaC）</td>
      </tr>
      <tr>
          <td>2</td>
          <td>重建了，但不確定跟之前一樣</td>
          <td>state 追蹤</td>
      </tr>
      <tr>
          <td>3</td>
          <td>測試時的操作打到了正式客戶</td>
          <td>環境分離</td>
      </tr>
      <tr>
          <td>4</td>
          <td>不知道誰有權限存取什麼</td>
          <td>身分與權限管控</td>
      </tr>
      <tr>
          <td>5</td>
          <td>出事了，查不到是誰在什麼時候改了什麼</td>
          <td>變更紀錄</td>
      </tr>
  </tbody>
</table>
<p>每一行用決策者熟悉的語言：影響範圍（幾個客戶）、恢復時間（幾小時 vs 幾天）、成本（工程師時間 + 停機損失）。這些維度是決策者日常在處理的，不需要翻譯。</p>
<p>情境遞進的優勢是它讓對方在自己的認知框架裡接收新資訊 — 管理者懂「服務掛了要能重建」，從這個已知出發、往「怎麼確保重建結果一致」「怎麼避免測試影響正式」遞進，每一步都建立在前一步的理解上。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>記住比喻、忘記風險</strong>：決策者開會時說「那個門禁卡的東西做了嗎」，但說不出它解決什麼問題、不做會怎樣。比喻成了一個替代理解的標籤。</li>
<li><strong>比喻限制討論深度</strong>：當需要討論「要不要把 production 拆到獨立帳號」時，辦公大樓比喻沒有對應物。討論被迫回到技術語言，之前的比喻投資歸零。</li>
<li><strong>溝通問題被錯誤歸因</strong>：推不動 infra 時，團隊以為是「沒解釋清楚」，於是加更多比喻。但問題其實是提案語言跟決策語言的框架不對，不是理解力不足。結果越比喻越偏離決策者的關注點。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>寫跨專業溝通文件時，如果發現自己在做以下事情，代表可能掉進了比喻堆疊：</p>
<ul>
<li>為每個技術概念都找一個日常比喻（「X 就像 Y」超過三個）</li>
<li>比喻之間需要互相引用才能成立（「記得剛才說的大樓嗎？這個是大樓裡的…」）</li>
<li>比喻崩解後要補救（「辦公大樓的比喻在這裡不完全適用，但大致上…」）</li>
<li>文章的主要結構是比喻而非情境（H2 標題是比喻物、不是管理問題）</li>
</ul>
<p>替代方式：每個概念用「它解什麼管理問題 + 不做的後果量級」一句話帶入，需要深入時用情境遞進展開。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人</a>：本卡的前提 — 跨專業溝通的對象是專業人士，比喻堆疊預設對方「聽不懂」</li>
<li>→ <a href="/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">#155 用語意標題引用、不用位置編號</a>：情境遞進跟語意引用同源 — 都是用內容本身（情境描述 / 語意標題）承載意義，不靠外部代理物（比喻 / 編號）</li>
<li>→ <a href="/blog/report/cross-round-review-stopping-signal/" data-link-title="跨輪 review 停止訊號是 frame 涵蓋、不是 finding 數遞減" data-link-desc="判斷「該不該再來一輪 review」的訊號是『frame 軸是否還有未動』、不是『上一輪 finding 變少』；多輪 review 的 ROI 不是 monotonically decreasing、而是 frame 切換的質性轉換 — Round N 用新 frame 通常仍會抓出 substantial finding、但內容從 surface compliance 往深層 structural issue 走；停止訊號是「下一輪可用的新 frame 已經想不出來」、不是 finding 數遞減；本卡填補 #114 / #126 / #147 沒覆蓋的「何時夠了」判讀缺口">#148 跨輪 review 停止訊號</a>：溝通層文章的 review 停止訊號之一是「比喻已經從輔助變成主體」— 這個 frame 在 Round 3 steelman 可以 catch</li>
</ul>
]]></content:encoded></item><item><title>技術教材要內嵌管理層可彙報的資訊</title><link>https://tarrragon.github.io/blog/report/technical-content-needs-management-reportable-info/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/technical-content-needs-management-reportable-info/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 infra 教學模組的管理層視角掃描。22 篇技術文章完成三輪審查後，掃描「管理層會問什麼」發現 10 處缺口，集中在成本量級和時程估算。限制：evidence 來自單一教學模組；成本數字（AWS 定價）有時效性，寫入教材後需要維護。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>技術教材的讀者通常需要向上彙報：為什麼要做這件事、要花多久、花多少錢、做完怎麼知道有效果。如果教材只講技術機制（怎麼寫 HCL、怎麼設 IAM），讀者學會了做法卻沒有素材向管理層說明，結果是做法正確但推不動——因為管理層沒拿到他們決策需要的資訯。&lt;/p>
&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>「這個選項每月多花多少？」&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>治理或 review 流程段&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;h2 id="情境">情境&lt;/h2>
&lt;p>infra 教學模組的 22 篇文章完成三輪寫作審查（compliance / cadence / steelman）後，從管理層視角掃描發現：&lt;/p>
&lt;ul>
&lt;li>NAT per-AZ vs 共享的段落講了可用性取捨，但沒說每個 NAT Gateway 月費約 $32——管理層看不到這筆錢的量級&lt;/li>
&lt;li>環境拆分的 retrofit 路徑講了操作步驟，但沒給時程估算——管理層問「要多久」時工程師得另外估&lt;/li>
&lt;li>tagging 講了為什麼要標，但沒說怎麼量化進度——管理層沒有可追蹤的指標&lt;/li>
&lt;li>跨帳號策略講了 SCP 怎麼設，但沒標記這是需要 CTO 對齊的決策——工程師可能自己決定了影響全組織的設定&lt;/li>
&lt;/ul>
&lt;p>這些缺口的共同模式是：技術內容本身完整，但缺少讓讀者「帶出去用」的彙報素材。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>在技術段落旁邊嵌入管理層資訊，用 1-2 句帶過量級，不獨立成段。嵌入而非集中的理由是：工程師讀到技術取捨時，同時拿到彙報素材，不需要翻到另一篇文章去找對應的商業論證。&lt;/p>
&lt;p>成本用量級而非精確數字。「每個 NAT Gateway 月費約 $32」比「$31.54/月」有用——量級讓管理層判斷這是百元級、千元級還是萬元級的決策，精確數字會隨定價調整而過時。&lt;/p>
&lt;p>時程用範圍而非單點。「1-2 週」比「10 天」誠實——infra 工作的時程變數主要來自 stateful 資源的數量和 drift 的嚴重度，這些在動工前無法精確預估。&lt;/p>
&lt;p>進度用可查詢的指標。「缺 tag 資源數 / 總資源數」比「我們在做 tagging」可追蹤——管理層需要的是趨勢線，不是狀態報告。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>推動卡在溝通&lt;/strong>：工程師學會了怎麼做 IaC，但在預算會議上說不出「導入要 2-3 天、第一個里程碑是一條指令重建環境」，管理層聽到的是一個沒有時間框架的技術提案&lt;/li>
&lt;li>&lt;strong>成本決策沒有量級感&lt;/strong>：multi-AZ RDS 費用翻倍、NAT per-AZ 三倍固定費——這些取捨在技術上有道理，但管理層需要知道「翻倍是從 $50 到 $100 還是從 $5000 到 $10000」才能判斷值不值得&lt;/li>
&lt;li>&lt;strong>進度不可見&lt;/strong>：infra 工作的中間產出（state 設好了、第一批資源 import 了）對管理層不可見，看起來像「花了兩週什麼都沒產出」。有量化指標（覆蓋率從 40% 到 70%）才能讓進度可追蹤&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>寫技術教材時，對每個取捨段落問兩個問題：&lt;/p>
&lt;ul>
&lt;li>「讀者的老闆會問什麼？」——如果答案是「多少錢」或「多久」而段落裡沒有，就是缺口&lt;/li>
&lt;li>「這個決定影響範圍多大？」——如果影響跨團隊或跨帳號，標記為需簽核的決策點&lt;/li>
&lt;/ul>
&lt;p>成本數字寫入教材後要標明是量級參考而非精確報價，避免讀者直接拿去做預算。雲端定價會變，量級通常穩定——$30/月級的服務不太會跳到 $300/月。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人&lt;/a>：本卡是該原則的延伸——讀者是專業人士，他們的工作流程包含向上彙報，教材應該支援這個流程&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/cross-expertise-communication-scenario-not-analogy/" data-link-title="跨專業溝通用情境遞進、不用比喻堆疊" data-link-desc="向非本領域的專業人士解釋技術議題時，減少術語並從簡單情境遞進到複雜情境，比堆疊比喻有效。比喻傳遞形狀但不傳遞嚴重性；情境遞進讓對方用自己熟悉的決策框架（成本、風險、時間）消化資訊。">跨專業溝通用情境遞進、不用比喻堆疊&lt;/a>：管理層資訊用量級和範圍表達，不用比喻——「月費約 $32」比「大約一頓飯的錢」精確且專業&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/09-driving-adoption/infra-business-justification/" data-link-title="infra 投資的商業論證" data-link-desc="用成本、風險、速度三條論述線把 infra 投資翻譯成商業語言，附一頁簡報邏輯與常見反對意見的回應">infra 投資的商業論證&lt;/a>：該文集中處理商業論證，本卡講的是把商業素材分散嵌入技術段落，兩者互補&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 infra 教學模組的管理層視角掃描。22 篇技術文章完成三輪審查後，掃描「管理層會問什麼」發現 10 處缺口，集中在成本量級和時程估算。限制：evidence 來自單一教學模組；成本數字（AWS 定價）有時效性，寫入教材後需要維護。</p>
<h2 id="核心原則">核心原則</h2>
<p>技術教材的讀者通常需要向上彙報：為什麼要做這件事、要花多久、花多少錢、做完怎麼知道有效果。如果教材只講技術機制（怎麼寫 HCL、怎麼設 IAM），讀者學會了做法卻沒有素材向管理層說明，結果是做法正確但推不動——因為管理層沒拿到他們決策需要的資訯。</p>
<p>管理層需要的資訊有四類，跟技術段落的位置關係明確：</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>緊接操作流程段，給範圍不給單點</td>
      </tr>
      <tr>
          <td>進度指標</td>
          <td>「怎麼知道有在進步？」</td>
          <td>治理或 review 流程段</td>
      </tr>
      <tr>
          <td>決策簽核點</td>
          <td>「這個決定要不要上層同意？」</td>
          <td>影響全組織的架構決策段</td>
      </tr>
  </tbody>
</table>
<h2 id="情境">情境</h2>
<p>infra 教學模組的 22 篇文章完成三輪寫作審查（compliance / cadence / steelman）後，從管理層視角掃描發現：</p>
<ul>
<li>NAT per-AZ vs 共享的段落講了可用性取捨，但沒說每個 NAT Gateway 月費約 $32——管理層看不到這筆錢的量級</li>
<li>環境拆分的 retrofit 路徑講了操作步驟，但沒給時程估算——管理層問「要多久」時工程師得另外估</li>
<li>tagging 講了為什麼要標，但沒說怎麼量化進度——管理層沒有可追蹤的指標</li>
<li>跨帳號策略講了 SCP 怎麼設，但沒標記這是需要 CTO 對齊的決策——工程師可能自己決定了影響全組織的設定</li>
</ul>
<p>這些缺口的共同模式是：技術內容本身完整，但缺少讓讀者「帶出去用」的彙報素材。</p>
<h2 id="理想做法">理想做法</h2>
<p>在技術段落旁邊嵌入管理層資訊，用 1-2 句帶過量級，不獨立成段。嵌入而非集中的理由是：工程師讀到技術取捨時，同時拿到彙報素材，不需要翻到另一篇文章去找對應的商業論證。</p>
<p>成本用量級而非精確數字。「每個 NAT Gateway 月費約 $32」比「$31.54/月」有用——量級讓管理層判斷這是百元級、千元級還是萬元級的決策，精確數字會隨定價調整而過時。</p>
<p>時程用範圍而非單點。「1-2 週」比「10 天」誠實——infra 工作的時程變數主要來自 stateful 資源的數量和 drift 的嚴重度，這些在動工前無法精確預估。</p>
<p>進度用可查詢的指標。「缺 tag 資源數 / 總資源數」比「我們在做 tagging」可追蹤——管理層需要的是趨勢線，不是狀態報告。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>推動卡在溝通</strong>：工程師學會了怎麼做 IaC，但在預算會議上說不出「導入要 2-3 天、第一個里程碑是一條指令重建環境」，管理層聽到的是一個沒有時間框架的技術提案</li>
<li><strong>成本決策沒有量級感</strong>：multi-AZ RDS 費用翻倍、NAT per-AZ 三倍固定費——這些取捨在技術上有道理，但管理層需要知道「翻倍是從 $50 到 $100 還是從 $5000 到 $10000」才能判斷值不值得</li>
<li><strong>進度不可見</strong>：infra 工作的中間產出（state 設好了、第一批資源 import 了）對管理層不可見，看起來像「花了兩週什麼都沒產出」。有量化指標（覆蓋率從 40% 到 70%）才能讓進度可追蹤</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>寫技術教材時，對每個取捨段落問兩個問題：</p>
<ul>
<li>「讀者的老闆會問什麼？」——如果答案是「多少錢」或「多久」而段落裡沒有，就是缺口</li>
<li>「這個決定影響範圍多大？」——如果影響跨團隊或跨帳號，標記為需簽核的決策點</li>
</ul>
<p>成本數字寫入教材後要標明是量級參考而非精確報價，避免讀者直接拿去做預算。雲端定價會變，量級通常穩定——$30/月級的服務不太會跳到 $300/月。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人</a>：本卡是該原則的延伸——讀者是專業人士，他們的工作流程包含向上彙報，教材應該支援這個流程</li>
<li>→ <a href="/blog/report/cross-expertise-communication-scenario-not-analogy/" data-link-title="跨專業溝通用情境遞進、不用比喻堆疊" data-link-desc="向非本領域的專業人士解釋技術議題時，減少術語並從簡單情境遞進到複雜情境，比堆疊比喻有效。比喻傳遞形狀但不傳遞嚴重性；情境遞進讓對方用自己熟悉的決策框架（成本、風險、時間）消化資訊。">跨專業溝通用情境遞進、不用比喻堆疊</a>：管理層資訊用量級和範圍表達，不用比喻——「月費約 $32」比「大約一頓飯的錢」精確且專業</li>
<li>→ <a href="/blog/infra/09-driving-adoption/infra-business-justification/" data-link-title="infra 投資的商業論證" data-link-desc="用成本、風險、速度三條論述線把 infra 投資翻譯成商業語言，附一頁簡報邏輯與常見反對意見的回應">infra 投資的商業論證</a>：該文集中處理商業論證，本卡講的是把商業素材分散嵌入技術段落，兩者互補</li>
</ul>
]]></content:encoded></item><item><title>多輪審查缺 outside-in 讀者 frame：六個系統性盲點</title><link>https://tarrragon.github.io/blog/report/review-lacks-outside-in-reader-frames/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/review-lacks-outside-in-reader-frames/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 infra 教學模組的完整生產週期 retrospective。43 篇文章 + 21 張知識卡經歷三輪多輪審查（compliance / cadence+冷讀 / steelman+outbound），審查通過後使用者連續指出六個 review 未 catch 的問題，每個都導致大量修改。限制：evidence 來自單一教學模組的一次完整週期，盲點清單可能不窮盡。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>多輪審查框架的所有 frame 從「已寫的內容」出發（inside-out）：字句層看已寫的字、cadence 看已寫的結構、fact-check 看已寫的事實、steelman 看已寫的論述。缺的是從「讀者的完整需求」出發的 frame（outside-in）：讀者是誰、讀者從哪裡來、讀者讀完後要做什麼、讀者搜尋什麼問題。&lt;/p>
&lt;p>inside-out 能保證已寫內容的品質，outside-in 才能發現「應該寫但沒寫」的缺口。兩者的盲區正交：inside-out 的盲區是缺口（看不到不存在的東西），outside-in 的盲區是細節（看不到已寫內容的字句問題）。完整的審查需要兩者交替。&lt;/p>
&lt;h2 id="六個盲點">六個盲點&lt;/h2>
&lt;h3 id="一宣導語氣通過三輪審查">一：宣導語氣通過三輪審查&lt;/h3>
&lt;p>三輪審查判定文章 clean，使用者指出「一台機器跑得好好的」「辦公大樓比喻」是對專業讀者的失配後，14 處需要重寫。&lt;/p>
&lt;p>keyword bank 的設計對象是字面違規。宣導語氣的問題在 register——用教導外行人的姿態對專業人士說話，字面上每個字都合規。reviewer 跟作者共享同一套文體直覺，都覺得「用故事帶入」是好的教學手法，同源盲區讓整批宣導語氣被合理化放行。&lt;/p>
&lt;p>缺的 frame：&lt;strong>reader-persona register 適配&lt;/strong>——指定具體讀者角色，問「這個人讀到這段會覺得被低估嗎」。&lt;/p>
&lt;h3 id="二管理層彙報資訊缺失">二：管理層彙報資訊缺失&lt;/h3>
&lt;p>所有 reviewer 檢查了技術正確性和寫作規範，沒有一個問「讀者讀完後能不能跟老闆說明為什麼要做、要花多久」。使用者提出後掃描出 10 處成本/時程缺口。&lt;/p>
&lt;p>review frame 全部從「內容品質」出發，沒有從「讀者的下游任務」出發。技術文章的讀者不只要「學會怎麼做」，還要「能向上彙報為什麼做」——後者是讀者的工作流程，品質導向的 review 結構性看不到。&lt;/p>
&lt;p>缺的 frame：&lt;strong>downstream-task 審查&lt;/strong>——問「讀者讀完後的下一個動作是什麼？他需要什麼素材？」&lt;/p>
&lt;h3 id="三接手-vs-自建是不同情境">三：接手 vs 自建是不同情境&lt;/h3>
&lt;p>模組負一定位為「還沒有 infra 的手動環境」，所有 reviewer 都接受了這個前提。使用者指出「接手前人的專案」是完全不同的操作情境後，獨立成一個橫切模組。&lt;/p>
&lt;p>review 的 scope 是「已寫的內容對不對」，不質疑結構本身的覆蓋範圍。steelman reviewer 問的是已有文章的論述有沒有漏洞，不是教材的讀者群有沒有被遺漏的情境。&lt;/p>
&lt;p>缺的 frame：&lt;strong>persona-coverage 審查&lt;/strong>——列出目標讀者可能進入教材的情境，檢查每個情境是否有對應入口。&lt;/p>
&lt;h3 id="四操作步驟缺工具指引">四：操作步驟缺工具指引&lt;/h3>
&lt;p>文章寫「拍下現況」「匯出資料庫」，reviewer 確認了邏輯正確性。使用者問「用什麼拍？怎麼拍？」後才發現停在 WHAT 層、沒到 HOW WITH WHAT 層。&lt;/p>
&lt;p>fact-check reviewer 驗證「描述是否正確」，不是「描述是否可執行」。「用 FTP 下載整站存進 Git」事實正確，但讀者照做時需要知道用哪個 FTP client、大檔案怎麼處理。&lt;/p>
&lt;p>缺的 frame：&lt;strong>executable-walkthrough 審查&lt;/strong>——假裝讀者從零照做，每步問「下一個動作是打開什麼軟體、輸入什麼指令」。&lt;/p>
&lt;h3 id="五概覽級深度不拆分">五：概覽級深度不拆分&lt;/h3>
&lt;p>350 行文章涵蓋四個面向，reviewer 認為「結構完整」。使用者指出「資料庫備份和安全管理其實都是大問題」後拆成 5 篇。後續又在 8 個模組發生同樣的拆分。&lt;/p>
&lt;p>reviewer 評估「這篇文章本身好不好」，不是「搜尋特定問題的讀者能不能找到足夠深度的內容」。一篇涵蓋四面向的文章通讀體驗良好，但搜尋「共享主機 MySQL 備份」的讀者需要專題文章。&lt;/p>
&lt;p>缺的 frame：&lt;strong>search-landing 審查&lt;/strong>——列出讀者可能搜尋的具體問題，檢查每個問題能不能落在聚焦的文章上。跟 cold-read (B&amp;rsquo;) 相關但不同——B&amp;rsquo; 看「落地後讀不讀得懂」，這裡看「能不能落地到足夠聚焦的內容」。&lt;/p>
&lt;h3 id="六讀者定位未預設">六：讀者定位未預設&lt;/h3>
&lt;p>「讀者是缺經驗的專業人士」這個原則在文章寫完、審查完、使用者反饋後才抽出來。它影響了語氣、比喻策略、管理層溝通——幾乎所有後續大修都源自這個原則。但它不在任何 reviewer prompt 裡。&lt;/p>
&lt;p>寫作規範定義了「怎麼寫」的規則，沒有定義「寫給誰」——讀者定位被當成隱性決定。LLM 預設用「教外行人」的姿態寫教學內容，這個預設不被 review 挑戰，因為 reviewer 也共享同一個預設。&lt;/p>
&lt;p>這不是 review frame 的問題——是&lt;strong>生成端的前提缺失&lt;/strong>。每個教學模組在第一篇文章生成前就應該顯式聲明讀者定位。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>在現有 inside-out review 框架之外，補五個 outside-in frame：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Frame&lt;/th>
 &lt;th>問什麼&lt;/th>
 &lt;th>在哪個 Round 跑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Reader persona + register&lt;/td>
 &lt;td>讀者讀到這段會覺得被低估嗎？&lt;/td>
 &lt;td>Round 2（讀者旅程）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Downstream task&lt;/td>
 &lt;td>讀者讀完後要做什麼、需要什麼素材？&lt;/td>
 &lt;td>Round 1（基線 audit）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Persona coverage&lt;/td>
 &lt;td>所有讀者情境都有入口嗎？&lt;/td>
 &lt;td>Round 3（outbound）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Executable walkthrough&lt;/td>
 &lt;td>讀者能從零照做嗎？每步的工具在嗎？&lt;/td>
 &lt;td>Round 2（操作型文章專用）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Search landing&lt;/td>
 &lt;td>搜尋特定問題能落在聚焦文章嗎？&lt;/td>
 &lt;td>Round 3（outbound）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>生成端的修正：每個教學模組在撰寫前顯式聲明「讀者定位文件」（一段話描述目標讀者的背景、已有能力、缺的經驗），讓生成和 review 都有可檢查的基準。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;p>六個盲點的修法總工程量遠超預防成本：14 處宣導語氣重寫 + 10 處管理層資訊補充 + 11 篇接手維運新文章 + 全模組工具補充 + 12 篇子文章拆分 + 3 篇入門/溝通層重寫。如果讀者定位在第一篇文章前就聲明、outside-in frame 在 Round 1 就跑，多數修改可以在初稿階段就避免。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>review 完成後如果使用者的第一個反饋是關於「內容缺口」而非「內容品質」，代表 review 框架偏向 inside-out。inside-out 的 review 報告 clean 只代表「已寫的內容沒問題」，不代表「該寫的都寫了」。&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 infra 教學模組的完整生產週期 retrospective。43 篇文章 + 21 張知識卡經歷三輪多輪審查（compliance / cadence+冷讀 / steelman+outbound），審查通過後使用者連續指出六個 review 未 catch 的問題，每個都導致大量修改。限制：evidence 來自單一教學模組的一次完整週期，盲點清單可能不窮盡。</p>
<h2 id="核心原則">核心原則</h2>
<p>多輪審查框架的所有 frame 從「已寫的內容」出發（inside-out）：字句層看已寫的字、cadence 看已寫的結構、fact-check 看已寫的事實、steelman 看已寫的論述。缺的是從「讀者的完整需求」出發的 frame（outside-in）：讀者是誰、讀者從哪裡來、讀者讀完後要做什麼、讀者搜尋什麼問題。</p>
<p>inside-out 能保證已寫內容的品質，outside-in 才能發現「應該寫但沒寫」的缺口。兩者的盲區正交：inside-out 的盲區是缺口（看不到不存在的東西），outside-in 的盲區是細節（看不到已寫內容的字句問題）。完整的審查需要兩者交替。</p>
<h2 id="六個盲點">六個盲點</h2>
<h3 id="一宣導語氣通過三輪審查">一：宣導語氣通過三輪審查</h3>
<p>三輪審查判定文章 clean，使用者指出「一台機器跑得好好的」「辦公大樓比喻」是對專業讀者的失配後，14 處需要重寫。</p>
<p>keyword bank 的設計對象是字面違規。宣導語氣的問題在 register——用教導外行人的姿態對專業人士說話，字面上每個字都合規。reviewer 跟作者共享同一套文體直覺，都覺得「用故事帶入」是好的教學手法，同源盲區讓整批宣導語氣被合理化放行。</p>
<p>缺的 frame：<strong>reader-persona register 適配</strong>——指定具體讀者角色，問「這個人讀到這段會覺得被低估嗎」。</p>
<h3 id="二管理層彙報資訊缺失">二：管理層彙報資訊缺失</h3>
<p>所有 reviewer 檢查了技術正確性和寫作規範，沒有一個問「讀者讀完後能不能跟老闆說明為什麼要做、要花多久」。使用者提出後掃描出 10 處成本/時程缺口。</p>
<p>review frame 全部從「內容品質」出發，沒有從「讀者的下游任務」出發。技術文章的讀者不只要「學會怎麼做」，還要「能向上彙報為什麼做」——後者是讀者的工作流程，品質導向的 review 結構性看不到。</p>
<p>缺的 frame：<strong>downstream-task 審查</strong>——問「讀者讀完後的下一個動作是什麼？他需要什麼素材？」</p>
<h3 id="三接手-vs-自建是不同情境">三：接手 vs 自建是不同情境</h3>
<p>模組負一定位為「還沒有 infra 的手動環境」，所有 reviewer 都接受了這個前提。使用者指出「接手前人的專案」是完全不同的操作情境後，獨立成一個橫切模組。</p>
<p>review 的 scope 是「已寫的內容對不對」，不質疑結構本身的覆蓋範圍。steelman reviewer 問的是已有文章的論述有沒有漏洞，不是教材的讀者群有沒有被遺漏的情境。</p>
<p>缺的 frame：<strong>persona-coverage 審查</strong>——列出目標讀者可能進入教材的情境，檢查每個情境是否有對應入口。</p>
<h3 id="四操作步驟缺工具指引">四：操作步驟缺工具指引</h3>
<p>文章寫「拍下現況」「匯出資料庫」，reviewer 確認了邏輯正確性。使用者問「用什麼拍？怎麼拍？」後才發現停在 WHAT 層、沒到 HOW WITH WHAT 層。</p>
<p>fact-check reviewer 驗證「描述是否正確」，不是「描述是否可執行」。「用 FTP 下載整站存進 Git」事實正確，但讀者照做時需要知道用哪個 FTP client、大檔案怎麼處理。</p>
<p>缺的 frame：<strong>executable-walkthrough 審查</strong>——假裝讀者從零照做，每步問「下一個動作是打開什麼軟體、輸入什麼指令」。</p>
<h3 id="五概覽級深度不拆分">五：概覽級深度不拆分</h3>
<p>350 行文章涵蓋四個面向，reviewer 認為「結構完整」。使用者指出「資料庫備份和安全管理其實都是大問題」後拆成 5 篇。後續又在 8 個模組發生同樣的拆分。</p>
<p>reviewer 評估「這篇文章本身好不好」，不是「搜尋特定問題的讀者能不能找到足夠深度的內容」。一篇涵蓋四面向的文章通讀體驗良好，但搜尋「共享主機 MySQL 備份」的讀者需要專題文章。</p>
<p>缺的 frame：<strong>search-landing 審查</strong>——列出讀者可能搜尋的具體問題，檢查每個問題能不能落在聚焦的文章上。跟 cold-read (B&rsquo;) 相關但不同——B&rsquo; 看「落地後讀不讀得懂」，這裡看「能不能落地到足夠聚焦的內容」。</p>
<h3 id="六讀者定位未預設">六：讀者定位未預設</h3>
<p>「讀者是缺經驗的專業人士」這個原則在文章寫完、審查完、使用者反饋後才抽出來。它影響了語氣、比喻策略、管理層溝通——幾乎所有後續大修都源自這個原則。但它不在任何 reviewer prompt 裡。</p>
<p>寫作規範定義了「怎麼寫」的規則，沒有定義「寫給誰」——讀者定位被當成隱性決定。LLM 預設用「教外行人」的姿態寫教學內容，這個預設不被 review 挑戰，因為 reviewer 也共享同一個預設。</p>
<p>這不是 review frame 的問題——是<strong>生成端的前提缺失</strong>。每個教學模組在第一篇文章生成前就應該顯式聲明讀者定位。</p>
<h2 id="理想做法">理想做法</h2>
<p>在現有 inside-out review 框架之外，補五個 outside-in frame：</p>
<table>
  <thead>
      <tr>
          <th>Frame</th>
          <th>問什麼</th>
          <th>在哪個 Round 跑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reader persona + register</td>
          <td>讀者讀到這段會覺得被低估嗎？</td>
          <td>Round 2（讀者旅程）</td>
      </tr>
      <tr>
          <td>Downstream task</td>
          <td>讀者讀完後要做什麼、需要什麼素材？</td>
          <td>Round 1（基線 audit）</td>
      </tr>
      <tr>
          <td>Persona coverage</td>
          <td>所有讀者情境都有入口嗎？</td>
          <td>Round 3（outbound）</td>
      </tr>
      <tr>
          <td>Executable walkthrough</td>
          <td>讀者能從零照做嗎？每步的工具在嗎？</td>
          <td>Round 2（操作型文章專用）</td>
      </tr>
      <tr>
          <td>Search landing</td>
          <td>搜尋特定問題能落在聚焦文章嗎？</td>
          <td>Round 3（outbound）</td>
      </tr>
  </tbody>
</table>
<p>生成端的修正：每個教學模組在撰寫前顯式聲明「讀者定位文件」（一段話描述目標讀者的背景、已有能力、缺的經驗），讓生成和 review 都有可檢查的基準。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<p>六個盲點的修法總工程量遠超預防成本：14 處宣導語氣重寫 + 10 處管理層資訊補充 + 11 篇接手維運新文章 + 全模組工具補充 + 12 篇子文章拆分 + 3 篇入門/溝通層重寫。如果讀者定位在第一篇文章前就聲明、outside-in frame 在 Round 1 就跑，多數修改可以在初稿階段就避免。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>review 完成後如果使用者的第一個反饋是關於「內容缺口」而非「內容品質」，代表 review 框架偏向 inside-out。inside-out 的 review 報告 clean 只代表「已寫的內容沒問題」，不代表「該寫的都寫了」。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人</a>：盲點一和六的直接修法</li>
<li>→ <a href="/blog/report/technical-content-needs-management-reportable-info/" data-link-title="技術教材要內嵌管理層可彙報的資訊" data-link-desc="技術文章的讀者不只要知道怎麼做，還要能向上彙報為什麼做、花多久、花多少。成本量級、時程估算、進度指標與需簽核的決策點應該嵌在技術段落旁邊，而非集中在另一篇溝通指南裡。">技術教材要內嵌管理層可彙報的資訊</a>：盲點二的直接修法</li>
<li>→ <a href="/blog/report/cross-expertise-communication-scenario-not-analogy/" data-link-title="跨專業溝通用情境遞進、不用比喻堆疊" data-link-desc="向非本領域的專業人士解釋技術議題時，減少術語並從簡單情境遞進到複雜情境，比堆疊比喻有效。比喻傳遞形狀但不傳遞嚴重性；情境遞進讓對方用自己熟悉的決策框架（成本、風險、時間）消化資訊。">跨專業溝通用情境遞進、不用比喻堆疊</a>：盲點一的溝通層修法</li>
<li>→ <a href="/blog/report/cross-round-review-stopping-signal/" data-link-title="跨輪 review 停止訊號是 frame 涵蓋、不是 finding 數遞減" data-link-desc="判斷「該不該再來一輪 review」的訊號是『frame 軸是否還有未動』、不是『上一輪 finding 變少』；多輪 review 的 ROI 不是 monotonically decreasing、而是 frame 切換的質性轉換 — Round N 用新 frame 通常仍會抓出 substantial finding、但內容從 surface compliance 往深層 structural issue 走；停止訊號是「下一輪可用的新 frame 已經想不出來」、不是 finding 數遞減；本卡填補 #114 / #126 / #147 沒覆蓋的「何時夠了」判讀缺口">#148 跨輪 review 停止訊號</a>：本卡揭露的是「停止訊號齊備但覆蓋不完整」的情境——frame 涵蓋度的判斷要包含 outside-in frame</li>
<li>→ <a href="/blog/report/review-miss-diagnose-design-vs-execution-gap/" data-link-title="Review 漏抓先分 design gap 與 execution gap、再決定改框架還是改執行" data-link-desc="Review 漏抓某類問題時，有兩個不同成因：design gap（框架根本沒有對應 frame）跟 execution gap（框架有 frame、但 reviewer 沒跑）。修法相反 —— design gap 要改框架（補 frame / keyword）、execution gap 要改執行（真的跑完該跑的輪）。診斷前先分清：把 execution gap 誤判成 design gap 會 framework bloat（一直加 frame 卻沒解決偷跑子集）、把 design gap 誤判成 execution gap 會永遠漏同類。常見陷阱是『加 keyword』感覺像進步、但對沒跑的輪毫無幫助。">#153 Review 漏抓先分 design gap 與 execution gap</a>：六個盲點全部是 design gap（框架缺 frame），不是 execution gap（有 frame 沒跑）</li>
<li>→ <a href="/blog/report/cold-reader-frame-vs-informed-reviewer/" data-link-title="多輪審查要有冷讀者 frame：知情 reviewer 看不見行話洩漏" data-link-desc="多輪審查模擬讀者時要分兩種：知情讀者（讀完全部、走旅程）與冷讀者（經搜尋或直連落在單篇、零脈絡）。知情 reviewer 會自動腦補脈絡，結構性看不見洩漏撰寫者預設前提的行話。原子 / Zettelkasten / glossary 等可被直連抵達的內容，必須額外跑冷讀 frame。">#168 多輪審查要有冷讀者 frame</a>：cold-read 是 outside-in 的一個實例（從零脈絡讀者出發），本卡把這個方向擴展到五個 frame</li>
</ul>
]]></content:encoded></item><item><title>操作指引的「怎麼做」要帶環境專屬的工具路徑</title><link>https://tarrragon.github.io/blog/report/operational-how-needs-environment-specific-tooling/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/operational-how-needs-environment-specific-tooling/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 infra 接手維運模組的兩次使用者反饋。第一次：文章寫「拍下現況」「建立本地環境」，使用者問「用什麼拍？非 Docker 怎麼做？」——補了 FTP client、本地環境工具選型表、備份自動化指令。第二次：文章寫「拍下完整現況（不動 prod）」，使用者問「如果不是在 container 上運作，怎麼取代拍下來的動作？」——補了 phpinfo 擷取、cron 記錄、AMI 快照、系統軟體清單。兩次反饋的根因相同。限制：evidence 來自單一教學模組。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>操作型教材的一個動作（「拍下現況」「匯出資料庫」「建立備份」）在不同執行環境裡對應完全不同的工具路徑：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>動作&lt;/th>
 &lt;th>Container 環境&lt;/th>
 &lt;th>VM（有 SSH）&lt;/th>
 &lt;th>共享主機（只有 FTP）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>拍下現況&lt;/td>
 &lt;td>&lt;code>docker commit&lt;/code> / image tag&lt;/td>
 &lt;td>AMI / machine image + 軟體清單&lt;/td>
 &lt;td>FTP mirror + phpinfo + cron 截圖&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>匯出資料庫&lt;/td>
 &lt;td>&lt;code>docker exec mysqldump&lt;/code>&lt;/td>
 &lt;td>&lt;code>mysqldump&lt;/code> via SSH&lt;/td>
 &lt;td>phpMyAdmin 匯出（有 timeout 限制）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>定期備份&lt;/td>
 &lt;td>volume snapshot / registry&lt;/td>
 &lt;td>cron + mysqldump + S3&lt;/td>
 &lt;td>本機排程 + lftp mirror&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查執行環境&lt;/td>
 &lt;td>&lt;code>docker inspect&lt;/code>&lt;/td>
 &lt;td>&lt;code>systemctl&lt;/code> / &lt;code>ss -tlnp&lt;/code>&lt;/td>
 &lt;td>phpinfo + 主機面板&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>只寫動作不寫工具路徑，等於把「選工具」的認知負擔轉嫁給讀者。讀者知道該做「拍下現況」，但坐在電腦前不知道該打開什麼軟體。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>infra 接手維運模組的文章經過三輪多輪審查（compliance / cadence / steelman），所有 reviewer 都判定操作步驟邏輯正確、順序合理。使用者兩次指出同一個缺口：&lt;/p>
&lt;p>第一次：「用什麼拍？怎麼拍？非 Docker 的環境怎麼做？」——三篇文章各補了 15-20 個工具提及，總修改量 138 行。&lt;/p>
&lt;p>第二次：「如果不是在 container 上運作，怎麼取代拍下來的動作？」——兩篇文章各補了環境設定的拍照方法（phpinfo、cron、SSL、AMI 快照、系統套件清單），總修改量 87 行。&lt;/p>
&lt;p>兩次的操作步驟在邏輯層都通過了 fact-check（「拍下現況」這個動作描述正確）和 steelman（「先拍後改」的順序合理）。問題出在「正確」跟「可執行」之間的落差——動作正確不代表讀者能執行，執行需要知道用什麼工具、在什麼環境限制下、有什麼替代方案。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>操作型教材的每一步至少帶一條環境專屬的工具路徑。如果教材涵蓋多種環境（container / VM / 共享主機），每一步要按環境分列工具，或明確標示「本篇的工具路徑適用於 X 環境、Y 環境的做法見另一篇」。&lt;/p>
&lt;p>寫操作步驟時的自測問題：「讀者坐在電腦前、打開這篇文章，下一個動作是啟動什麼軟體或輸入什麼指令？」如果答案是「看情況」，就要把「情況」展開成環境分支。&lt;/p>
&lt;p>生成端的檢查清單（寫完每一步後問）：&lt;/p>
&lt;ul>
&lt;li>這一步的工具是什麼？有沒有寫出來？&lt;/li>
&lt;li>這個工具在目標環境可用嗎？（共享主機沒有 SSH、沒有 CLI）&lt;/li>
&lt;li>如果不可用，替代工具是什麼？&lt;/li>
&lt;li>工具的操作有沒有環境限制？（phpMyAdmin 的 timeout、FTP 的非原子上傳）&lt;/li>
&lt;/ul>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>兩次同根因的返工&lt;/strong>：同一個缺口（動作缺工具）被使用者指出兩次、修了兩輪，因為第一輪只補了「用什麼工具」沒補「在不同環境怎麼替代」。如果第一次就按環境分列工具路徑，第二次不會發生。&lt;/li>
&lt;li>&lt;strong>審查結構性盲區&lt;/strong>：fact-check 驗「正確性」、steelman 驗「完整性」，兩者都不驗「可執行性」。操作型文章的品質門檻比概念型文章多一層——除了正確和完整，還要可執行。這層門檻需要 executable-walkthrough frame（見&lt;a href="https://tarrragon.github.io/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">outside-in reader frames report&lt;/a>）。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>操作型文章裡出現以下動詞但沒有跟著工具名稱或指令，就是候選缺口：&lt;/p>
&lt;p>「拍下」「匯出」「備份」「部署」「掃描」「盤點」「監控」「還原」&lt;/p>
&lt;p>這些動詞描述的都是需要工具才能完成的操作。概念型文章用這些動詞是在描述能力（「IaC 的責任是讓環境可重建」），操作型文章用這些動詞是在指引行動——行動需要工具。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">多輪審查缺 outside-in 讀者 frame&lt;/a>：本卡是該卡盲點四（操作步驟缺工具指引）的具體展開，補充了「為什麼同根因會被指出兩次」的機制&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/technical-content-needs-management-reportable-info/" data-link-title="技術教材要內嵌管理層可彙報的資訊" data-link-desc="技術文章的讀者不只要知道怎麼做，還要能向上彙報為什麼做、花多久、花多少。成本量級、時程估算、進度指標與需簽核的決策點應該嵌在技術段落旁邊，而非集中在另一篇溝通指南裡。">技術教材要內嵌管理層可彙報的資訊&lt;/a>：工具路徑跟管理層資訊是同一類缺口——都是「讀者的下游任務需要但文章沒給的素材」&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人&lt;/a>：專業人士缺的不是概念（「要備份」），而是特定環境下的操作經驗（「共享主機沒有 mysqldump 時怎麼備份」）&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 infra 接手維運模組的兩次使用者反饋。第一次：文章寫「拍下現況」「建立本地環境」，使用者問「用什麼拍？非 Docker 怎麼做？」——補了 FTP client、本地環境工具選型表、備份自動化指令。第二次：文章寫「拍下完整現況（不動 prod）」，使用者問「如果不是在 container 上運作，怎麼取代拍下來的動作？」——補了 phpinfo 擷取、cron 記錄、AMI 快照、系統軟體清單。兩次反饋的根因相同。限制：evidence 來自單一教學模組。</p>
<h2 id="核心原則">核心原則</h2>
<p>操作型教材的一個動作（「拍下現況」「匯出資料庫」「建立備份」）在不同執行環境裡對應完全不同的工具路徑：</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>Container 環境</th>
          <th>VM（有 SSH）</th>
          <th>共享主機（只有 FTP）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>拍下現況</td>
          <td><code>docker commit</code> / image tag</td>
          <td>AMI / machine image + 軟體清單</td>
          <td>FTP mirror + phpinfo + cron 截圖</td>
      </tr>
      <tr>
          <td>匯出資料庫</td>
          <td><code>docker exec mysqldump</code></td>
          <td><code>mysqldump</code> via SSH</td>
          <td>phpMyAdmin 匯出（有 timeout 限制）</td>
      </tr>
      <tr>
          <td>定期備份</td>
          <td>volume snapshot / registry</td>
          <td>cron + mysqldump + S3</td>
          <td>本機排程 + lftp mirror</td>
      </tr>
      <tr>
          <td>查執行環境</td>
          <td><code>docker inspect</code></td>
          <td><code>systemctl</code> / <code>ss -tlnp</code></td>
          <td>phpinfo + 主機面板</td>
      </tr>
  </tbody>
</table>
<p>只寫動作不寫工具路徑，等於把「選工具」的認知負擔轉嫁給讀者。讀者知道該做「拍下現況」，但坐在電腦前不知道該打開什麼軟體。</p>
<h2 id="情境">情境</h2>
<p>infra 接手維運模組的文章經過三輪多輪審查（compliance / cadence / steelman），所有 reviewer 都判定操作步驟邏輯正確、順序合理。使用者兩次指出同一個缺口：</p>
<p>第一次：「用什麼拍？怎麼拍？非 Docker 的環境怎麼做？」——三篇文章各補了 15-20 個工具提及，總修改量 138 行。</p>
<p>第二次：「如果不是在 container 上運作，怎麼取代拍下來的動作？」——兩篇文章各補了環境設定的拍照方法（phpinfo、cron、SSL、AMI 快照、系統套件清單），總修改量 87 行。</p>
<p>兩次的操作步驟在邏輯層都通過了 fact-check（「拍下現況」這個動作描述正確）和 steelman（「先拍後改」的順序合理）。問題出在「正確」跟「可執行」之間的落差——動作正確不代表讀者能執行，執行需要知道用什麼工具、在什麼環境限制下、有什麼替代方案。</p>
<h2 id="理想做法">理想做法</h2>
<p>操作型教材的每一步至少帶一條環境專屬的工具路徑。如果教材涵蓋多種環境（container / VM / 共享主機），每一步要按環境分列工具，或明確標示「本篇的工具路徑適用於 X 環境、Y 環境的做法見另一篇」。</p>
<p>寫操作步驟時的自測問題：「讀者坐在電腦前、打開這篇文章，下一個動作是啟動什麼軟體或輸入什麼指令？」如果答案是「看情況」，就要把「情況」展開成環境分支。</p>
<p>生成端的檢查清單（寫完每一步後問）：</p>
<ul>
<li>這一步的工具是什麼？有沒有寫出來？</li>
<li>這個工具在目標環境可用嗎？（共享主機沒有 SSH、沒有 CLI）</li>
<li>如果不可用，替代工具是什麼？</li>
<li>工具的操作有沒有環境限制？（phpMyAdmin 的 timeout、FTP 的非原子上傳）</li>
</ul>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>兩次同根因的返工</strong>：同一個缺口（動作缺工具）被使用者指出兩次、修了兩輪，因為第一輪只補了「用什麼工具」沒補「在不同環境怎麼替代」。如果第一次就按環境分列工具路徑，第二次不會發生。</li>
<li><strong>審查結構性盲區</strong>：fact-check 驗「正確性」、steelman 驗「完整性」，兩者都不驗「可執行性」。操作型文章的品質門檻比概念型文章多一層——除了正確和完整，還要可執行。這層門檻需要 executable-walkthrough frame（見<a href="/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">outside-in reader frames report</a>）。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>操作型文章裡出現以下動詞但沒有跟著工具名稱或指令，就是候選缺口：</p>
<p>「拍下」「匯出」「備份」「部署」「掃描」「盤點」「監控」「還原」</p>
<p>這些動詞描述的都是需要工具才能完成的操作。概念型文章用這些動詞是在描述能力（「IaC 的責任是讓環境可重建」），操作型文章用這些動詞是在指引行動——行動需要工具。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">多輪審查缺 outside-in 讀者 frame</a>：本卡是該卡盲點四（操作步驟缺工具指引）的具體展開，補充了「為什麼同根因會被指出兩次」的機制</li>
<li>→ <a href="/blog/report/technical-content-needs-management-reportable-info/" data-link-title="技術教材要內嵌管理層可彙報的資訊" data-link-desc="技術文章的讀者不只要知道怎麼做，還要能向上彙報為什麼做、花多久、花多少。成本量級、時程估算、進度指標與需簽核的決策點應該嵌在技術段落旁邊，而非集中在另一篇溝通指南裡。">技術教材要內嵌管理層可彙報的資訊</a>：工具路徑跟管理層資訊是同一類缺口——都是「讀者的下游任務需要但文章沒給的素材」</li>
<li>→ <a href="/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人</a>：專業人士缺的不是概念（「要備份」），而是特定環境下的操作經驗（「共享主機沒有 mysqldump 時怎麼備份」）</li>
</ul>
]]></content:encoded></item><item><title>跨 surface 鏡像的連結轉換 mapping 要窮盡、不能靠猜</title><link>https://tarrragon.github.io/blog/report/mirror-link-mapping-must-be-exhaustive/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mirror-link-mapping-must-be-exhaustive/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 compositional-writing skill 鏡像同步的連續三次 CI 失敗。每次都是 &lt;code>mdtools cards&lt;/code> 報 broken link、每次都修幾個 mapping、每次都以為修完了、下次 push 又報新的。限制：evidence 來自單一 skill 的鏡像同步。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>跨 surface 鏡像（&lt;code>.claude/skills/&lt;/code> → &lt;code>content/skills/&lt;/code>）的連結轉換有一個結構性約束：原始檔用相對連結（&lt;code>references/principles/xxx.md&lt;/code>，portable 設計），鏡像檔要用真實路徑（&lt;code>/report/slug/&lt;/code>，blog 內連結）。兩者的 slug 不一定一致——principle 卡的檔名是 skill 內部命名，report 卡的檔名是 blog 內部命名，兩者獨立演化。&lt;/p>
&lt;p>mapping 不完整時的失敗模式是&lt;strong>每次修一批漏一批&lt;/strong>：第一次靠 slug 精確匹配轉了 20 個、漏了 5 個不匹配的；第二次手動補了 3 個已知的 mismatch、漏了 2 個不知道對應哪張 report 的；第三次才把最後 2 個找到。三次 CI 失敗、三次 commit、三次 push。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>compositional-writing 的 SKILL.md 有約 25 個 &lt;code>references/principles/xxx.md&lt;/code> 連結。鏡像同步時：&lt;/p>
&lt;ul>
&lt;li>第一輪：slug 精確匹配轉了 ~20 個，剩 5 個報 broken&lt;/li>
&lt;li>修了 3 個已知的 slug mismatch（teaching-prose → teaching-register、cross-expertise-scenario → cross-expertise-communication 等）&lt;/li>
&lt;li>CI 仍報 3 個 broken：decorative-symbols-keyword-bank、risk-asymmetric-audit-standard&lt;/li>
&lt;li>錯誤判斷「沒有對應 report 卡」→ 改成純文字&lt;/li>
&lt;li>使用者指出「一定有對應的 report 卡，找的方式有問題」&lt;/li>
&lt;li>重新用 rg 搜索 report 內容而非 slug 匹配，找到：decorative-symbols → visual-tool-error-layer-alignment、risk-asymmetric → security-teaching-rigor-asymmetry&lt;/li>
&lt;/ul>
&lt;p>根因是 mapping 的搜尋策略——只用 slug 精確匹配和少量已知 mismatch，沒有窮盡所有 principle 檔案。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>建立 mapping 時用窮盡策略，不靠碰運氣：&lt;/p>
&lt;ol>
&lt;li>列出所有 principle 檔案：&lt;code>ls .claude/skills/&amp;lt;name&amp;gt;/references/principles/*.md&lt;/code>&lt;/li>
&lt;li>對每個 principle，讀它的標題和「來源」段，找出它從哪張 report 卡抽出&lt;/li>
&lt;li>用 report 卡的內容搜尋（&lt;code>rg &amp;quot;關鍵詞&amp;quot; content/report/&lt;/code>）而非 slug 匹配&lt;/li>
&lt;li>把完整 mapping 寫進腳本的 case 語句&lt;/li>
&lt;/ol>
&lt;p>mapping 的維護：每次新增 principle 卡時，同步在腳本裡加一行 case。&lt;/p>
&lt;p>自動化輔助：腳本跑完後如果有 WARN（unresolved），不要直接 commit——先確認這些 unresolved 是真的沒有 report 卡、還是 mapping 漏了。「沒有對應 report 卡」是需要證明的結論、不是搜尋失敗的預設。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>三次 CI 失敗&lt;/strong>：每次以為修完、push 後又報新的 broken link，因為每次只修當前報錯的、沒有窮盡檢查全部 mapping&lt;/li>
&lt;li>&lt;strong>錯誤結論「沒有 report 卡」&lt;/strong>：slug 不匹配被誤判為「不存在」，實際是搜尋方式太窄（只靠檔名比對）。差點把有效連結改成純文字、損失 blog 內的導航&lt;/li>
&lt;li>&lt;strong>修法引入新問題&lt;/strong>：改成純文字後 mdtools cards 不報錯了，但讀者在 blog 上看到的是不可點擊的文字、失去了導航功能&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>腳本輸出 WARN 時，問自己：「我用了什麼搜尋策略？只用 slug 比對、還是也搜了 report 卡的內容和標題？」如果只用 slug 比對，WARN 可能是 false negative（有對應卡但 slug 不同）。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/cross-surface-recontextualize-not-transplant/" data-link-title="跨 surface 同主題內容要重新語境化、不是搬運：逐字相同句是未語境化的訊號" data-link-desc="同一個原則要同時存在於兩個 surface（教材章節與 agent 協議、blog 卡與 skill 卡）時、規範說「各寫一份、語境化在各 surface 內」— 語境化的可操作判準是：句子要跟著該 surface 的讀者與用途改寫、兩邊逐字相同的句子是未語境化的候選訊號、命中後逐處判讀。逐字搬運讓兩份內容形成沒人宣告的隱性同源、改一邊另一邊 silent 漂移、且兩邊都沒有為自己的讀者最佳化。">跨 surface 同主題內容要重新語境化、不是搬運&lt;/a>：鏡像連結轉換是跨 surface 語境化的一部分——portable skill 用相對連結、blog 用真實路徑、兩者的連結策略不同&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/operational-how-needs-environment-specific-tooling/" data-link-title="操作指引的「怎麼做」要帶環境專屬的工具路徑" data-link-desc="操作型教材說「拍下現況」「匯出資料庫」「建立備份」時，不同執行環境（container / VM / 共享主機）的工具路徑完全不同。只寫動作不寫工具，讀者知道該做什麼但做不到。這個缺口在 fact-check 和 steelman 審查裡結構性隱形，因為動作本身在邏輯層是正確的。">操作指引要帶環境專屬工具路徑&lt;/a>：同一個「搜尋 mapping」動作在不同條件下（slug 匹配 vs 內容搜尋）的工具路徑不同，跟操作步驟缺工具指引是同構問題&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 compositional-writing skill 鏡像同步的連續三次 CI 失敗。每次都是 <code>mdtools cards</code> 報 broken link、每次都修幾個 mapping、每次都以為修完了、下次 push 又報新的。限制：evidence 來自單一 skill 的鏡像同步。</p>
<h2 id="核心原則">核心原則</h2>
<p>跨 surface 鏡像（<code>.claude/skills/</code> → <code>content/skills/</code>）的連結轉換有一個結構性約束：原始檔用相對連結（<code>references/principles/xxx.md</code>，portable 設計），鏡像檔要用真實路徑（<code>/report/slug/</code>，blog 內連結）。兩者的 slug 不一定一致——principle 卡的檔名是 skill 內部命名，report 卡的檔名是 blog 內部命名，兩者獨立演化。</p>
<p>mapping 不完整時的失敗模式是<strong>每次修一批漏一批</strong>：第一次靠 slug 精確匹配轉了 20 個、漏了 5 個不匹配的；第二次手動補了 3 個已知的 mismatch、漏了 2 個不知道對應哪張 report 的；第三次才把最後 2 個找到。三次 CI 失敗、三次 commit、三次 push。</p>
<h2 id="情境">情境</h2>
<p>compositional-writing 的 SKILL.md 有約 25 個 <code>references/principles/xxx.md</code> 連結。鏡像同步時：</p>
<ul>
<li>第一輪：slug 精確匹配轉了 ~20 個，剩 5 個報 broken</li>
<li>修了 3 個已知的 slug mismatch（teaching-prose → teaching-register、cross-expertise-scenario → cross-expertise-communication 等）</li>
<li>CI 仍報 3 個 broken：decorative-symbols-keyword-bank、risk-asymmetric-audit-standard</li>
<li>錯誤判斷「沒有對應 report 卡」→ 改成純文字</li>
<li>使用者指出「一定有對應的 report 卡，找的方式有問題」</li>
<li>重新用 rg 搜索 report 內容而非 slug 匹配，找到：decorative-symbols → visual-tool-error-layer-alignment、risk-asymmetric → security-teaching-rigor-asymmetry</li>
</ul>
<p>根因是 mapping 的搜尋策略——只用 slug 精確匹配和少量已知 mismatch，沒有窮盡所有 principle 檔案。</p>
<h2 id="理想做法">理想做法</h2>
<p>建立 mapping 時用窮盡策略，不靠碰運氣：</p>
<ol>
<li>列出所有 principle 檔案：<code>ls .claude/skills/&lt;name&gt;/references/principles/*.md</code></li>
<li>對每個 principle，讀它的標題和「來源」段，找出它從哪張 report 卡抽出</li>
<li>用 report 卡的內容搜尋（<code>rg &quot;關鍵詞&quot; content/report/</code>）而非 slug 匹配</li>
<li>把完整 mapping 寫進腳本的 case 語句</li>
</ol>
<p>mapping 的維護：每次新增 principle 卡時，同步在腳本裡加一行 case。</p>
<p>自動化輔助：腳本跑完後如果有 WARN（unresolved），不要直接 commit——先確認這些 unresolved 是真的沒有 report 卡、還是 mapping 漏了。「沒有對應 report 卡」是需要證明的結論、不是搜尋失敗的預設。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>三次 CI 失敗</strong>：每次以為修完、push 後又報新的 broken link，因為每次只修當前報錯的、沒有窮盡檢查全部 mapping</li>
<li><strong>錯誤結論「沒有 report 卡」</strong>：slug 不匹配被誤判為「不存在」，實際是搜尋方式太窄（只靠檔名比對）。差點把有效連結改成純文字、損失 blog 內的導航</li>
<li><strong>修法引入新問題</strong>：改成純文字後 mdtools cards 不報錯了，但讀者在 blog 上看到的是不可點擊的文字、失去了導航功能</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>腳本輸出 WARN 時，問自己：「我用了什麼搜尋策略？只用 slug 比對、還是也搜了 report 卡的內容和標題？」如果只用 slug 比對，WARN 可能是 false negative（有對應卡但 slug 不同）。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/cross-surface-recontextualize-not-transplant/" data-link-title="跨 surface 同主題內容要重新語境化、不是搬運：逐字相同句是未語境化的訊號" data-link-desc="同一個原則要同時存在於兩個 surface（教材章節與 agent 協議、blog 卡與 skill 卡）時、規範說「各寫一份、語境化在各 surface 內」— 語境化的可操作判準是：句子要跟著該 surface 的讀者與用途改寫、兩邊逐字相同的句子是未語境化的候選訊號、命中後逐處判讀。逐字搬運讓兩份內容形成沒人宣告的隱性同源、改一邊另一邊 silent 漂移、且兩邊都沒有為自己的讀者最佳化。">跨 surface 同主題內容要重新語境化、不是搬運</a>：鏡像連結轉換是跨 surface 語境化的一部分——portable skill 用相對連結、blog 用真實路徑、兩者的連結策略不同</li>
<li>→ <a href="/blog/report/operational-how-needs-environment-specific-tooling/" data-link-title="操作指引的「怎麼做」要帶環境專屬的工具路徑" data-link-desc="操作型教材說「拍下現況」「匯出資料庫」「建立備份」時，不同執行環境（container / VM / 共享主機）的工具路徑完全不同。只寫動作不寫工具，讀者知道該做什麼但做不到。這個缺口在 fact-check 和 steelman 審查裡結構性隱形，因為動作本身在邏輯層是正確的。">操作指引要帶環境專屬工具路徑</a>：同一個「搜尋 mapping」動作在不同條件下（slug 匹配 vs 內容搜尋）的工具路徑不同，跟操作步驟缺工具指引是同構問題</li>
</ul>
]]></content:encoded></item><item><title>先建 report 卡再進 skill、不是先改 skill 再補 report</title><link>https://tarrragon.github.io/blog/report/report-before-skill-not-after/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/report-before-skill-not-after/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 infra 教學模組的完整生產週期。期間多次出現「先改了 skill、事後才補 report」的操作順序，導致 skill 裡的新規則在一段時間內沒有 report 卡的根據可追溯。限制：evidence 來自單一專案的 skill 演進過程。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>report 卡是原則的 SSoT——它記錄了這個原則是從什麼情境抽出來的、根因是什麼、理想做法是什麼、不這樣做的麻煩是什麼。skill 是 report 的操作化引用——它把 report 裡的原則轉成 reviewer prompt 的審查維度、生成端的檢查清單、keyword bank 的 grep pattern。&lt;/p>
&lt;p>兩者的關係是 report → skill，不是 skill → report：&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>report → skill&lt;/td>
 &lt;td>從情境抽出原則、再操作化進工具&lt;/td>
 &lt;td>低：原則有根據、可追溯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>skill → report&lt;/td>
 &lt;td>先在工具裡加規則、事後補根據&lt;/td>
 &lt;td>高：規則缺根據、report 容易被跳過&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>infra 模組生產期間的操作順序：&lt;/p>
&lt;ul>
&lt;li>使用者指出「寫作語氣要調整」→ 直接改 compositional-writing skill 加 keyword bank → 兩天後才補 report 卡&lt;/li>
&lt;li>使用者指出「管理層資訊缺失」→ 直接補文章 → 之後才建 report 卡 → 再之後才進 skill&lt;/li>
&lt;li>使用者指出「鏡像連結 mapping 不完整」→ 修了腳本 → 之後才建 report 卡 → 還沒進 skill 就又出下一個問題&lt;/li>
&lt;/ul>
&lt;p>每次都能運作，但 report 卡的建立被擠到「有空再做」的位置，而非流程的第一步。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>標準操作流程從 report 卡開始：&lt;/p>
&lt;ol>
&lt;li>發現問題或收到使用者反饋&lt;/li>
&lt;li>建 report 卡（情境 → 根因 → 理想做法 → 判讀徵兆）&lt;/li>
&lt;li>評估是否進 skill（哪個 skill、哪個段落）&lt;/li>
&lt;li>修改 skill、引用 report 卡路徑&lt;/li>
&lt;li>推送 skill 庫 + 同步鏡像&lt;/li>
&lt;/ol>
&lt;p>report 卡先於 skill 修改，確保每條 skill 規則都有可追溯的根據。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>規則缺根據&lt;/strong>：skill 裡加了一條 grep pattern 但沒有 report 卡解釋為什麼要加，三個月後某人問「這條規則的由來是什麼」時答不出來&lt;/li>
&lt;li>&lt;strong>report 被跳過&lt;/strong>：「先改 skill 再補 report」的順序讓 report 變成事後文件，容易被「下次再補」拖延到永遠不補&lt;/li>
&lt;li>&lt;strong>skill 的 Version 歷史缺引用&lt;/strong>：Version 條目寫「加了 X 規則」但沒有 &lt;code>per [report 卡名](/report/slug/)&lt;/code> 的引用，讀者無法回溯規則的來源情境&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>如果 skill 的 Version 歷史裡有一條更新沒有附 report 卡的引用，代表流程順序反了。每條 skill 更新都應該能回溯到一張 report 卡——即使那張卡是同一個 commit 建的。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/mirror-link-mapping-must-be-exhaustive/" data-link-title="跨 surface 鏡像的連結轉換 mapping 要窮盡、不能靠猜" data-link-desc="skill 鏡像從 .claude/skills/ 複製到 content/skills/ 時，references/principles/ 的相對連結要轉成 /report/ 的真實路徑。mapping table 不完整會讓 CI 反覆 broken link，每次修一批漏一批。窮盡 mapping 的方法是列出所有 principle 檔案再逐一找對應 report 卡，不是靠 slug 精確匹配碰運氣。">跨 surface 鏡像的連結轉換 mapping 要窮盡&lt;/a>：鏡像同步是 skill 更新流程的最後一步，report 建卡是第一步&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">多輪審查缺 outside-in 讀者 frame&lt;/a>：六個盲點的修法都是「先建 report → 再進 skill」的順序&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 infra 教學模組的完整生產週期。期間多次出現「先改了 skill、事後才補 report」的操作順序，導致 skill 裡的新規則在一段時間內沒有 report 卡的根據可追溯。限制：evidence 來自單一專案的 skill 演進過程。</p>
<h2 id="核心原則">核心原則</h2>
<p>report 卡是原則的 SSoT——它記錄了這個原則是從什麼情境抽出來的、根因是什麼、理想做法是什麼、不這樣做的麻煩是什麼。skill 是 report 的操作化引用——它把 report 裡的原則轉成 reviewer prompt 的審查維度、生成端的檢查清單、keyword bank 的 grep pattern。</p>
<p>兩者的關係是 report → skill，不是 skill → report：</p>
<table>
  <thead>
      <tr>
          <th>方向</th>
          <th>意義</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>report → skill</td>
          <td>從情境抽出原則、再操作化進工具</td>
          <td>低：原則有根據、可追溯</td>
      </tr>
      <tr>
          <td>skill → report</td>
          <td>先在工具裡加規則、事後補根據</td>
          <td>高：規則缺根據、report 容易被跳過</td>
      </tr>
  </tbody>
</table>
<h2 id="情境">情境</h2>
<p>infra 模組生產期間的操作順序：</p>
<ul>
<li>使用者指出「寫作語氣要調整」→ 直接改 compositional-writing skill 加 keyword bank → 兩天後才補 report 卡</li>
<li>使用者指出「管理層資訊缺失」→ 直接補文章 → 之後才建 report 卡 → 再之後才進 skill</li>
<li>使用者指出「鏡像連結 mapping 不完整」→ 修了腳本 → 之後才建 report 卡 → 還沒進 skill 就又出下一個問題</li>
</ul>
<p>每次都能運作，但 report 卡的建立被擠到「有空再做」的位置，而非流程的第一步。</p>
<h2 id="理想做法">理想做法</h2>
<p>標準操作流程從 report 卡開始：</p>
<ol>
<li>發現問題或收到使用者反饋</li>
<li>建 report 卡（情境 → 根因 → 理想做法 → 判讀徵兆）</li>
<li>評估是否進 skill（哪個 skill、哪個段落）</li>
<li>修改 skill、引用 report 卡路徑</li>
<li>推送 skill 庫 + 同步鏡像</li>
</ol>
<p>report 卡先於 skill 修改，確保每條 skill 規則都有可追溯的根據。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>規則缺根據</strong>：skill 裡加了一條 grep pattern 但沒有 report 卡解釋為什麼要加，三個月後某人問「這條規則的由來是什麼」時答不出來</li>
<li><strong>report 被跳過</strong>：「先改 skill 再補 report」的順序讓 report 變成事後文件，容易被「下次再補」拖延到永遠不補</li>
<li><strong>skill 的 Version 歷史缺引用</strong>：Version 條目寫「加了 X 規則」但沒有 <code>per [report 卡名](/report/slug/)</code> 的引用，讀者無法回溯規則的來源情境</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>如果 skill 的 Version 歷史裡有一條更新沒有附 report 卡的引用，代表流程順序反了。每條 skill 更新都應該能回溯到一張 report 卡——即使那張卡是同一個 commit 建的。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/mirror-link-mapping-must-be-exhaustive/" data-link-title="跨 surface 鏡像的連結轉換 mapping 要窮盡、不能靠猜" data-link-desc="skill 鏡像從 .claude/skills/ 複製到 content/skills/ 時，references/principles/ 的相對連結要轉成 /report/ 的真實路徑。mapping table 不完整會讓 CI 反覆 broken link，每次修一批漏一批。窮盡 mapping 的方法是列出所有 principle 檔案再逐一找對應 report 卡，不是靠 slug 精確匹配碰運氣。">跨 surface 鏡像的連結轉換 mapping 要窮盡</a>：鏡像同步是 skill 更新流程的最後一步，report 建卡是第一步</li>
<li>→ <a href="/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">多輪審查缺 outside-in 讀者 frame</a>：六個盲點的修法都是「先建 report → 再進 skill」的順序</li>
</ul>
]]></content:encoded></item><item><title>常識是相對於讀者背景的、不是作者背景的</title><link>https://tarrragon.github.io/blog/report/common-knowledge-is-relative-to-reader-background/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/common-knowledge-is-relative-to-reader-background/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 infra 教學模組的知識卡建卡過程。最初建了 14 張卡（IaC、VPC、subnet 等雲端 infra 概念），後續補了 7 張（route-table、SCP 等進階概念），但 legacy 環境的工具（phpMyAdmin、FileZilla、.htaccess、.env）一直沒建卡——作者和 reviewer 都覺得「這些是常識、不需要建卡」。使用者指出「現代工程師不一定認識這些老工具，接手的工程師不一定是寫 PHP 的」後才意識到這是建卡判準的盲點。限制：evidence 來自單一教學模組。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>知識卡的建卡判準是「目標讀者群裡最不熟悉的那個人能不能理解」，不是「作者覺得這個術語夠不夠常見」。&lt;/p>
&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>.htaccess&lt;/td>
 &lt;td>PHP / Apache 工程師&lt;/td>
 &lt;td>Node.js / Go / Python 工程師&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>DNS TTL&lt;/td>
 &lt;td>後端 / DevOps&lt;/td>
 &lt;td>前端 / 行動端&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>phpMyAdmin&lt;/td>
 &lt;td>PHP 生態&lt;/td>
 &lt;td>任何非 PHP 背景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cron&lt;/td>
 &lt;td>Linux 使用者&lt;/td>
 &lt;td>Windows 背景、純雲端開發者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>opcache&lt;/td>
 &lt;td>PHP 效能調校者&lt;/td>
 &lt;td>任何非 PHP 背景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>VPC&lt;/td>
 &lt;td>雲端工程師&lt;/td>
 &lt;td>只用過 PaaS 的開發者&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>作者的盲點是：寫某個領域的教材時，作者已經熟悉該領域的術語，把自己的熟悉度投射成「讀者也知道」。reviewer 如果跟作者背景相似（同源），會有相同的盲點。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>infra 接手維運模組的文章大量使用 phpMyAdmin、FileZilla、.htaccess、.env、cPanel 等術語。三輪多輪審查（compliance / cadence / steelman）都沒有指出這些術語需要知識卡——因為 reviewer 跟作者都把它們當成「寫過 PHP 就知道的東西」。&lt;/p>
&lt;p>使用者指出：「接手的工程師不一定是寫 PHP 的，他只是被叫來接專案。」這句話揭露了建卡判準的錯誤假設——教材的讀者不是「寫 PHP 的人」，而是「被指派接手一個 PHP 專案的任何背景的工程師」。後者的術語熟悉度遠低於前者。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>建卡判準用「最不熟悉的目標讀者」而非「作者的熟悉度」：&lt;/p>
&lt;ol>
&lt;li>定義目標讀者群的背景光譜（接手維運的讀者可能是 PHP / Node / Go / Python / 前端背景）&lt;/li>
&lt;li>對每個術語問：「光譜裡最不熟悉的那端，看到這個詞能理解嗎？」&lt;/li>
&lt;li>如果答案是「部分讀者需要查」，就建卡——卡的成本很低（40-50 行），但缺卡讓讀者卡住的成本很高（關掉頁面去 Google、可能找到不準確的解釋）&lt;/li>
&lt;/ol>
&lt;p>建卡的邊際成本判斷：一張 40 行的卡花 5 分鐘寫，讀者搜尋到一張不存在的卡花 10 分鐘去外部找答案（可能不準確）。建卡的 CP 值幾乎總是正的——除非術語真的只在一篇文章出現一次且已有完整 inline 解釋。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>讀者流失&lt;/strong>：非 PHP 背景的工程師讀到第三個不認識的術語（phpMyAdmin → .htaccess → opcache）後放棄，因為教材假設了他沒有的背景知識&lt;/li>
&lt;li>&lt;strong>外部搜尋的風險&lt;/strong>：讀者離開教材去 Google「什麼是 .htaccess」，找到的解釋可能跟教材的上下文不一致（例如 Google 結果講的是 Apache 2.2 的語法，教材用的是 2.4+）&lt;/li>
&lt;li>&lt;strong>review 結構性漏抓&lt;/strong>：同源 reviewer 跟作者有相同的「常識」盲點，加再多輪也抓不到——這是 outside-in 的 reader-persona frame 才能 catch 的問題&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>寫教材時如果某個術語「不需要解釋」的直覺反應來得太快，問自己：「一個完全不同背景的專業人士看到這個詞，能理解嗎？」如果要想超過三秒才能回答，建卡。&lt;/p>
&lt;p>另一個徵兆：如果教材的目標讀者群跨越多個技術背景（如「任何被指派接手 legacy 專案的工程師」），幾乎所有領域特定的術語都需要建卡——因為沒有任何一個術語對所有背景都是常識。&lt;/p>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人&lt;/a>：讀者有系統思考能力、缺的是特定領域的術語和經驗。知識卡補的正是這個缺口——給他一個 40 行的快速入口，讓他回到教材繼續讀&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">多輪審查缺 outside-in 讀者 frame&lt;/a>：「作者覺得是常識」是 inside-out 盲點的又一個實例——inside-out 從內容看品質（術語用得對不對），outside-in 從讀者看缺口（讀者認不認識這個術語）&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/atomic-note-needs-situational-entry/" data-link-title="原子筆記要有向上的議題入口：讀者要知道為何讀這張、何時會撞到" data-link-desc="承載知識的原子筆記不是字典條目。每張卡（或其上層）要回答「你在討論什麼議題、撞到什麼問題，才需要這個知識」——從情境進入，而非從定義進入。做法是建『議題 hub』上層筆記討論問題、再分流到術語卡，術語卡頂部回指議題。">原子筆記要有向上的議題入口&lt;/a>：知識卡從情境進入（「你在接手 legacy 專案時會遇到這個工具」），不是字典定義&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 infra 教學模組的知識卡建卡過程。最初建了 14 張卡（IaC、VPC、subnet 等雲端 infra 概念），後續補了 7 張（route-table、SCP 等進階概念），但 legacy 環境的工具（phpMyAdmin、FileZilla、.htaccess、.env）一直沒建卡——作者和 reviewer 都覺得「這些是常識、不需要建卡」。使用者指出「現代工程師不一定認識這些老工具，接手的工程師不一定是寫 PHP 的」後才意識到這是建卡判準的盲點。限制：evidence 來自單一教學模組。</p>
<h2 id="核心原則">核心原則</h2>
<p>知識卡的建卡判準是「目標讀者群裡最不熟悉的那個人能不能理解」，不是「作者覺得這個術語夠不夠常見」。</p>
<p>常識是相對於背景的：</p>
<table>
  <thead>
      <tr>
          <th>術語</th>
          <th>哪些背景覺得是常識</th>
          <th>哪些背景需要解釋</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>.htaccess</td>
          <td>PHP / Apache 工程師</td>
          <td>Node.js / Go / Python 工程師</td>
      </tr>
      <tr>
          <td>DNS TTL</td>
          <td>後端 / DevOps</td>
          <td>前端 / 行動端</td>
      </tr>
      <tr>
          <td>phpMyAdmin</td>
          <td>PHP 生態</td>
          <td>任何非 PHP 背景</td>
      </tr>
      <tr>
          <td>cron</td>
          <td>Linux 使用者</td>
          <td>Windows 背景、純雲端開發者</td>
      </tr>
      <tr>
          <td>opcache</td>
          <td>PHP 效能調校者</td>
          <td>任何非 PHP 背景</td>
      </tr>
      <tr>
          <td>VPC</td>
          <td>雲端工程師</td>
          <td>只用過 PaaS 的開發者</td>
      </tr>
  </tbody>
</table>
<p>作者的盲點是：寫某個領域的教材時，作者已經熟悉該領域的術語，把自己的熟悉度投射成「讀者也知道」。reviewer 如果跟作者背景相似（同源），會有相同的盲點。</p>
<h2 id="情境">情境</h2>
<p>infra 接手維運模組的文章大量使用 phpMyAdmin、FileZilla、.htaccess、.env、cPanel 等術語。三輪多輪審查（compliance / cadence / steelman）都沒有指出這些術語需要知識卡——因為 reviewer 跟作者都把它們當成「寫過 PHP 就知道的東西」。</p>
<p>使用者指出：「接手的工程師不一定是寫 PHP 的，他只是被叫來接專案。」這句話揭露了建卡判準的錯誤假設——教材的讀者不是「寫 PHP 的人」，而是「被指派接手一個 PHP 專案的任何背景的工程師」。後者的術語熟悉度遠低於前者。</p>
<h2 id="理想做法">理想做法</h2>
<p>建卡判準用「最不熟悉的目標讀者」而非「作者的熟悉度」：</p>
<ol>
<li>定義目標讀者群的背景光譜（接手維運的讀者可能是 PHP / Node / Go / Python / 前端背景）</li>
<li>對每個術語問：「光譜裡最不熟悉的那端，看到這個詞能理解嗎？」</li>
<li>如果答案是「部分讀者需要查」，就建卡——卡的成本很低（40-50 行），但缺卡讓讀者卡住的成本很高（關掉頁面去 Google、可能找到不準確的解釋）</li>
</ol>
<p>建卡的邊際成本判斷：一張 40 行的卡花 5 分鐘寫，讀者搜尋到一張不存在的卡花 10 分鐘去外部找答案（可能不準確）。建卡的 CP 值幾乎總是正的——除非術語真的只在一篇文章出現一次且已有完整 inline 解釋。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>讀者流失</strong>：非 PHP 背景的工程師讀到第三個不認識的術語（phpMyAdmin → .htaccess → opcache）後放棄，因為教材假設了他沒有的背景知識</li>
<li><strong>外部搜尋的風險</strong>：讀者離開教材去 Google「什麼是 .htaccess」，找到的解釋可能跟教材的上下文不一致（例如 Google 結果講的是 Apache 2.2 的語法，教材用的是 2.4+）</li>
<li><strong>review 結構性漏抓</strong>：同源 reviewer 跟作者有相同的「常識」盲點，加再多輪也抓不到——這是 outside-in 的 reader-persona frame 才能 catch 的問題</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>寫教材時如果某個術語「不需要解釋」的直覺反應來得太快，問自己：「一個完全不同背景的專業人士看到這個詞，能理解嗎？」如果要想超過三秒才能回答，建卡。</p>
<p>另一個徵兆：如果教材的目標讀者群跨越多個技術背景（如「任何被指派接手 legacy 專案的工程師」），幾乎所有領域特定的術語都需要建卡——因為沒有任何一個術語對所有背景都是常識。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">讀者是缺經驗的專業人士、不是外行人</a>：讀者有系統思考能力、缺的是特定領域的術語和經驗。知識卡補的正是這個缺口——給他一個 40 行的快速入口，讓他回到教材繼續讀</li>
<li>→ <a href="/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">多輪審查缺 outside-in 讀者 frame</a>：「作者覺得是常識」是 inside-out 盲點的又一個實例——inside-out 從內容看品質（術語用得對不對），outside-in 從讀者看缺口（讀者認不認識這個術語）</li>
<li>→ <a href="/blog/report/atomic-note-needs-situational-entry/" data-link-title="原子筆記要有向上的議題入口：讀者要知道為何讀這張、何時會撞到" data-link-desc="承載知識的原子筆記不是字典條目。每張卡（或其上層）要回答「你在討論什麼議題、撞到什麼問題，才需要這個知識」——從情境進入，而非從定義進入。做法是建『議題 hub』上層筆記討論問題、再分流到術語卡，術語卡頂部回指議題。">原子筆記要有向上的議題入口</a>：知識卡從情境進入（「你在接手 legacy 專案時會遇到這個工具」），不是字典定義</li>
</ul>
]]></content:encoded></item><item><title>一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好</title><link>https://tarrragon.github.io/blog/report/single-function-per-article-sop-vs-retrospective/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/single-function-per-article-sop-vs-retrospective/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自四篇方法論文章同時塞 SOP 和驗證紀錄、導致兩種讀者都服務不好的分類檢討。limitation：evidence 來自同一個 blog 的四篇文章，都是寫作方法論主題。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>操作步驟（SOP：結構模板、流程 checklist、怎麼做）和演化紀錄（retrospective：批次驗證數據、實際跑出來學到什麼）服務不同讀者。混寫本身不一定失敗（SRE 手冊和 postmortem 模板都成功交織兩者），但在本 repo 的場景下會造成兩個具體問題：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>SOP 同時存在於 skill 和文章裡，改 skill 時文章沒同步更新、兩處內容分歧&lt;/strong>。這是主要痛點。&lt;/li>
&lt;li>沒有清楚的分節標示時，讀者要跳過大量「不是給我看的」段落。&lt;/li>
&lt;/ol>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>四篇 &lt;code>posts/&lt;/code> 文章的共通症狀：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>SOP 段內容&lt;/th>
 &lt;th>Retrospective 段內容&lt;/th>
 &lt;th>混合比例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>migration-playbook-methodology&lt;/td>
 &lt;td>6 type 結構模板、diff dimension audit 步驟&lt;/td>
 &lt;td>三輪 batch 驗證、cadence dogfood、self-aware limitation update&lt;/td>
 &lt;td>SOP 40% / retro 60%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>vendor-deep-article-methodology&lt;/td>
 &lt;td>選題判準、6 段結構、寫作流程 7 step&lt;/td>
 &lt;td>兩輪 batch 驗證、跨兩輪 cadence 對照&lt;/td>
 &lt;td>SOP 35% / retro 65%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>verification-driven-cli-tool-articles&lt;/td>
 &lt;td>分類 → fixture → 標註 → gotcha 回寫流程&lt;/td>
 &lt;td>「為什麼官方 docs 不夠」+ 實測落差清單&lt;/td>
 &lt;td>SOP 70% / retro 30%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ci-silent-hang-diagnosis&lt;/td>
 &lt;td>無明確 SOP（不可重複流程）&lt;/td>
 &lt;td>完整 case study + 原則提取&lt;/td>
 &lt;td>不適用此分類&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>前三篇的共通模式：文章開頭像方法論手冊（SOP 感），中段突然變成「第一輪 demo 驗證」「第二輪 batch 對照」（retrospective 感），讀者在兩種模式之間切換的認知成本高。&lt;/p>
&lt;p>第四篇 ci-silent-hang 是不同問題 — 它不是功能混合，是資料夾歸類錯（debugging case study 放在 &lt;code>posts/&lt;/code> 而非 &lt;code>work-log/&lt;/code>）。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>功能拆分到對應的 surface：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>功能&lt;/th>
 &lt;th>Surface&lt;/th>
 &lt;th>讀者&lt;/th>
 &lt;th>格式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SOP（操作步驟）&lt;/td>
 &lt;td>&lt;code>.claude/skills/&lt;/code>&lt;/td>
 &lt;td>Claude runtime + 人類執行者&lt;/td>
 &lt;td>Skill 格式（H1 + body、portable）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retrospective（驗證證據）&lt;/td>
 &lt;td>&lt;code>posts/&lt;/code> 或 &lt;code>content/skills/&lt;/code> 鏡像&lt;/td>
 &lt;td>人類讀者&lt;/td>
 &lt;td>文章格式（Hugo frontmatter）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Debugging case&lt;/td>
 &lt;td>&lt;code>work-log/&lt;/code>&lt;/td>
 &lt;td>人類讀者&lt;/td>
 &lt;td>事件紀錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>抽象原則&lt;/td>
 &lt;td>&lt;code>report/&lt;/code>&lt;/td>
 &lt;td>所有讀者&lt;/td>
 &lt;td>Report 卡片&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>拆分後的文章只保留 retrospective 段（去掉跟 skill 重複的 SOP 步驟），開頭引用 skill 路徑建立 context。去掉 SOP 後文章若不能獨立成篇（冷讀者讀不懂、或只剩表格沒有判讀），降級成 &lt;code>content/skills/&lt;/code> 鏡像。&lt;/p>
&lt;p>已有先例：&lt;code>case-first-agent-team-review-workflow&lt;/code> 已經走這條路 — 文章是方法論敘事、skill 是操作步驟、兩者共存互連。&lt;/p>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>機器讀者找不到步驟&lt;/strong>：Claude runtime 透過 skill 觸發操作流程；SOP 埋在文章的 retrospective 段落裡，觸發路徑不通&lt;/li>
&lt;li>&lt;strong>人類讀者跳著讀&lt;/strong>：讀者進來看「migration playbook 怎麼寫」，要跳過三輪 batch 驗證表才找到 6 type 結構模板；或者進來看「為什麼三輪 batch collapse 率不同」，要跳過 diff dimension audit 步驟才到驗證段&lt;/li>
&lt;li>&lt;strong>維護雙份&lt;/strong>：SOP 同時存在於 skill 和文章裡（migration-playbook 已發生），改 skill 時文章沒同步更新，兩處內容分歧&lt;/li>
&lt;li>&lt;strong>新文章不知道放哪&lt;/strong>：下一篇方法論文章也會自然累積 SOP + retrospective，如果沒有拆分慣例，就會繼續混寫&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>寫方法論文章時，如果文章裡同時出現以下兩類段落，就是功能混合的訊號：&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自四篇方法論文章同時塞 SOP 和驗證紀錄、導致兩種讀者都服務不好的分類檢討。limitation：evidence 來自同一個 blog 的四篇文章，都是寫作方法論主題。</p>
<h2 id="核心原則">核心原則</h2>
<p>操作步驟（SOP：結構模板、流程 checklist、怎麼做）和演化紀錄（retrospective：批次驗證數據、實際跑出來學到什麼）服務不同讀者。混寫本身不一定失敗（SRE 手冊和 postmortem 模板都成功交織兩者），但在本 repo 的場景下會造成兩個具體問題：</p>
<ol>
<li><strong>SOP 同時存在於 skill 和文章裡，改 skill 時文章沒同步更新、兩處內容分歧</strong>。這是主要痛點。</li>
<li>沒有清楚的分節標示時，讀者要跳過大量「不是給我看的」段落。</li>
</ol>
<h2 id="情境">情境</h2>
<p>四篇 <code>posts/</code> 文章的共通症狀：</p>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>SOP 段內容</th>
          <th>Retrospective 段內容</th>
          <th>混合比例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>migration-playbook-methodology</td>
          <td>6 type 結構模板、diff dimension audit 步驟</td>
          <td>三輪 batch 驗證、cadence dogfood、self-aware limitation update</td>
          <td>SOP 40% / retro 60%</td>
      </tr>
      <tr>
          <td>vendor-deep-article-methodology</td>
          <td>選題判準、6 段結構、寫作流程 7 step</td>
          <td>兩輪 batch 驗證、跨兩輪 cadence 對照</td>
          <td>SOP 35% / retro 65%</td>
      </tr>
      <tr>
          <td>verification-driven-cli-tool-articles</td>
          <td>分類 → fixture → 標註 → gotcha 回寫流程</td>
          <td>「為什麼官方 docs 不夠」+ 實測落差清單</td>
          <td>SOP 70% / retro 30%</td>
      </tr>
      <tr>
          <td>ci-silent-hang-diagnosis</td>
          <td>無明確 SOP（不可重複流程）</td>
          <td>完整 case study + 原則提取</td>
          <td>不適用此分類</td>
      </tr>
  </tbody>
</table>
<p>前三篇的共通模式：文章開頭像方法論手冊（SOP 感），中段突然變成「第一輪 demo 驗證」「第二輪 batch 對照」（retrospective 感），讀者在兩種模式之間切換的認知成本高。</p>
<p>第四篇 ci-silent-hang 是不同問題 — 它不是功能混合，是資料夾歸類錯（debugging case study 放在 <code>posts/</code> 而非 <code>work-log/</code>）。</p>
<h2 id="理想做法">理想做法</h2>
<p>功能拆分到對應的 surface：</p>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>Surface</th>
          <th>讀者</th>
          <th>格式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SOP（操作步驟）</td>
          <td><code>.claude/skills/</code></td>
          <td>Claude runtime + 人類執行者</td>
          <td>Skill 格式（H1 + body、portable）</td>
      </tr>
      <tr>
          <td>Retrospective（驗證證據）</td>
          <td><code>posts/</code> 或 <code>content/skills/</code> 鏡像</td>
          <td>人類讀者</td>
          <td>文章格式（Hugo frontmatter）</td>
      </tr>
      <tr>
          <td>Debugging case</td>
          <td><code>work-log/</code></td>
          <td>人類讀者</td>
          <td>事件紀錄</td>
      </tr>
      <tr>
          <td>抽象原則</td>
          <td><code>report/</code></td>
          <td>所有讀者</td>
          <td>Report 卡片</td>
      </tr>
  </tbody>
</table>
<p>拆分後的文章只保留 retrospective 段（去掉跟 skill 重複的 SOP 步驟），開頭引用 skill 路徑建立 context。去掉 SOP 後文章若不能獨立成篇（冷讀者讀不懂、或只剩表格沒有判讀），降級成 <code>content/skills/</code> 鏡像。</p>
<p>已有先例：<code>case-first-agent-team-review-workflow</code> 已經走這條路 — 文章是方法論敘事、skill 是操作步驟、兩者共存互連。</p>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>機器讀者找不到步驟</strong>：Claude runtime 透過 skill 觸發操作流程；SOP 埋在文章的 retrospective 段落裡，觸發路徑不通</li>
<li><strong>人類讀者跳著讀</strong>：讀者進來看「migration playbook 怎麼寫」，要跳過三輪 batch 驗證表才找到 6 type 結構模板；或者進來看「為什麼三輪 batch collapse 率不同」，要跳過 diff dimension audit 步驟才到驗證段</li>
<li><strong>維護雙份</strong>：SOP 同時存在於 skill 和文章裡（migration-playbook 已發生），改 skill 時文章沒同步更新，兩處內容分歧</li>
<li><strong>新文章不知道放哪</strong>：下一篇方法論文章也會自然累積 SOP + retrospective，如果沒有拆分慣例，就會繼續混寫</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>寫方法論文章時，如果文章裡同時出現以下兩類段落，就是功能混合的訊號：</p>
<ol>
<li><strong>步驟型段落</strong>：「Step 1 → Step 2 → Step 3」、結構模板、checklist、「照這個跑」</li>
<li><strong>證據型段落</strong>：「第 N 輪 batch 驗證」、「N/N collapse 率」、「跨兩輪對照」、「self-aware limitation update」</li>
</ol>
<p>另一個徵兆：文章已有對應的 skill（或適合建 skill），但文章裡仍重複 skill 的 SOP 內容。</p>
<p>本徵兆適用 <code>posts/</code> 方法論文章。<code>report/</code> 卡片的修法步驟 + case 證據並存是正常形態（report 卡格式本就含情境 + 原則 + 修法）。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/article-body-must-align-with-title-commitment/" data-link-title="文章主體要對齊標題承諾、WRAP 內部分析不該喧賓奪主" data-link-desc="文章標題對讀者做了承諾、文章主體必須對齊這個承諾。WRAP 內部分析（Widen Options &#43; Reality Test 含 prior 引用 &#43; evidence weight）即使方法論做得好、如果不是標題承諾的內容、就不該佔文章主體—屬於 scope mismatch、跟 process metadata 暴露（#141）的議題分開。附帶議題：當 WRAP 內部分析喧賓奪主、為了支撐 prior 容易引入沒實際出處的 source citation；把 WRAP 內部分析從主體移除、hallucination 風險自然降低。是 #141 的姊妹卡—#141 處理章節標題 surface、本卡處理章節內容 scope。">#142 文章主體要對齊標題承諾</a>：#142 處理「章節內容偏離標題」，本卡處理「整篇文章功能定位混合」— #142 是段落層、本卡是文章層，同屬「內容要對齊承諾」家族</li>
<li>→ <a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">#122 cadence 同質化</a>：本卡四篇文章中的 retrospective 段大量引用 #122 的 cadence 概念；拆分後 retrospective 段落成為 #122 的 evidence 文件，SOP 進 skill 不再重複</li>
<li>→ <a href="/blog/report/summary-section-signals-scattered-prose/" data-link-title="教材的『重點 / 總結』段是內容發散的訊號、該重組正文不該補丁" data-link-desc="教材尾端的『重點』『一句話總結』段、若功能是重述前面已講過的內容、就是正文組織不佳的補丁。讀者需要回頭被提醒、代表概念在正文裡散掉了 —— 該做的是重拆正文段落、把概念在它該出現的位置一次講清、而不是另開總結段替發散的正文善後。判準：刪掉總結段後正文若仍站得住、證明總結本就冗餘；若站不住、是正文要重組、不是總結要補。處理總結段內容時先分『提醒 vs 概念』—— 純提醒（養成習慣 / 回頭確認）刪、有概念價值的（為何這樣設計）併回它本該所屬的正文位置。">#154 教材的重點/總結段是內容發散訊號</a>：#154 說「刪掉總結段看正文站不站得住」，本卡的對應操作是「刪掉 SOP 段看 retrospective 站不站得住」— 同類「減法測試」判準</li>
<li>→ AGENTS.md 跨 surface 內容處理原則：本卡的拆分動作跨 <code>.claude/skills/</code> 和 <code>content/</code> 兩個 surface，各自獨立、不交叉引用</li>
</ul>
]]></content:encoded></item><item><title>Log 時間真空是 silent hang 訊號、happy log 是 anti-signal</title><link>https://tarrragon.github.io/blog/report/time-vacuum-in-logs-signals-silent-hang/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/time-vacuum-in-logs-signals-silent-hang/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 blog CI 的 Playwright install step 反覆 timeout 事件。Playwright 1.59 在 Node.js 24.16.0 上 extract-zip silent hang，表面看是「下載太慢 / timeout 太緊」，實際是 upstream regression。limitation：evidence 來自單一 CI 事件，但 silent hang 模式在 Docker build、cron job、database migration 等場景都出現過。&lt;/p>
&lt;p>完整 case study 見 &lt;a href="https://tarrragon.github.io/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/" data-link-title="CI step silent hang：時間真空才是訊號、happy log 反而是 anti-signal" data-link-desc="CI step 跑很久才 timeout、最後一行卻是「下載 100% / build succeeded」這種 happy log 時回來。判讀：別急著加 timeout，先算最後一行到 cancel 的時間真空、確認是 silent hang，再用症狀詞查 upstream issue。同方向修法連 fail 2 次就是停手回資料層的訊號。">CI step silent hang&lt;/a>。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>非互動 process 的 log 輸出中，最後一行成功訊息（happy log）到被外部 cancel 之間的大段時間無輸出（時間真空），是 silent hang 的判讀訊號。&lt;/p>
&lt;p>技術人員習慣在 log 裡搜尋 error keyword 找失敗原因。但 silent hang 沒有 error keyword — process 沒 crash，只是不再做任何事。辨識 silent hang 需要轉換訊號類型：從「訊息內容」轉到「訊息時序」。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>CI step 跑了 15 分鐘被 timeout cancel。最後一行 log 是「chromium 下載 100% 完成」— 這是 happy log，直覺判斷是「下載慢、timeout 太緊」。加了 cache + bump timeout 到 25 分鐘，仍然頂到上限被 cancel。&lt;/p>
&lt;p>回頭看 detailed log 的 timestamp：&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">2026-05-27T09:59:44.110Z | 100% of 170.4 MiB
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">2026-05-27T10:24:15.201Z ##[error]The operation was canceled.&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>24 分 31 秒的時間真空。下載 2 秒完成，之後 process 完全沒有任何 log 輸出直到被 cancel。&lt;/p>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>CI step timeout 時，先抓四個 timestamp 判斷是否 silent hang，再決定修法：&lt;/p>
&lt;ol>
&lt;li>Step 開始的 timestamp&lt;/li>
&lt;li>Step 結束（cancel / fail）的 timestamp&lt;/li>
&lt;li>最後一行有意義輸出的 timestamp&lt;/li>
&lt;li>計算 #3 到 #2 之間的時間真空&lt;/li>
&lt;/ol>
&lt;p>真空相對該 step 正常輸出節奏明顯異常（CI extract 類場景通常秒級輸出、真空超過數分鐘即可疑）且最後一行是 happy log → silent hang 嫌疑高 → 用症狀詞查 upstream issue tracker，不是加 timeout。&lt;/p>
&lt;p>三類 timeout 模式的修法不同：&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>進度持續、最後階段到 timeout&lt;/td>
 &lt;td>時間真的不夠&lt;/td>
 &lt;td>bump timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>有失敗訊息之後 timeout&lt;/td>
 &lt;td>code 邏輯錯&lt;/td>
 &lt;td>看訊息修&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>最後一行 happy log 之後大段時間真空&lt;/td>
 &lt;td>silent hang&lt;/td>
 &lt;td>查 upstream issue tracker&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>反覆加 timeout&lt;/strong>：每次都「差一點」（頂到上限），每次都以為「timeout 不夠」，實際上 process 永遠不會自己結束&lt;/li>
&lt;li>&lt;strong>Cache 是假瓶頸&lt;/strong>：直覺判斷「下載慢 → 加 cache」，但瓶頸在 extract hang（下載只花 2 秒）&lt;/li>
&lt;li>&lt;strong>False positive 越雕越精緻&lt;/strong>：cache key 調整、timeout 微調、retry 策略 — 每一步單看合理，合起來是把錯誤假設越做越細&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>兩個訊號同時出現時，應該先排除 silent hang 再提其他解法：&lt;/p></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 blog CI 的 Playwright install step 反覆 timeout 事件。Playwright 1.59 在 Node.js 24.16.0 上 extract-zip silent hang，表面看是「下載太慢 / timeout 太緊」，實際是 upstream regression。limitation：evidence 來自單一 CI 事件，但 silent hang 模式在 Docker build、cron job、database migration 等場景都出現過。</p>
<p>完整 case study 見 <a href="/blog/work-log/ci-step-silent-hang%E6%99%82%E9%96%93%E7%9C%9F%E7%A9%BA%E6%89%8D%E6%98%AF%E8%A8%8A%E8%99%9Fhappy-log-%E5%8F%8D%E8%80%8C%E6%98%AF-anti-signal/" data-link-title="CI step silent hang：時間真空才是訊號、happy log 反而是 anti-signal" data-link-desc="CI step 跑很久才 timeout、最後一行卻是「下載 100% / build succeeded」這種 happy log 時回來。判讀：別急著加 timeout，先算最後一行到 cancel 的時間真空、確認是 silent hang，再用症狀詞查 upstream issue。同方向修法連 fail 2 次就是停手回資料層的訊號。">CI step silent hang</a>。</p>
<h2 id="核心原則">核心原則</h2>
<p>非互動 process 的 log 輸出中，最後一行成功訊息（happy log）到被外部 cancel 之間的大段時間無輸出（時間真空），是 silent hang 的判讀訊號。</p>
<p>技術人員習慣在 log 裡搜尋 error keyword 找失敗原因。但 silent hang 沒有 error keyword — process 沒 crash，只是不再做任何事。辨識 silent hang 需要轉換訊號類型：從「訊息內容」轉到「訊息時序」。</p>
<h2 id="情境">情境</h2>
<p>CI step 跑了 15 分鐘被 timeout cancel。最後一行 log 是「chromium 下載 100% 完成」— 這是 happy log，直覺判斷是「下載慢、timeout 太緊」。加了 cache + bump timeout 到 25 分鐘，仍然頂到上限被 cancel。</p>
<p>回頭看 detailed log 的 timestamp：</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">2026-05-27T09:59:44.110Z  | 100% of 170.4 MiB
</span></span><span class="line"><span class="ln">2</span><span class="cl">2026-05-27T10:24:15.201Z  ##[error]The operation was canceled.</span></span></code></pre></div><p>24 分 31 秒的時間真空。下載 2 秒完成，之後 process 完全沒有任何 log 輸出直到被 cancel。</p>
<h2 id="理想做法">理想做法</h2>
<p>CI step timeout 時，先抓四個 timestamp 判斷是否 silent hang，再決定修法：</p>
<ol>
<li>Step 開始的 timestamp</li>
<li>Step 結束（cancel / fail）的 timestamp</li>
<li>最後一行有意義輸出的 timestamp</li>
<li>計算 #3 到 #2 之間的時間真空</li>
</ol>
<p>真空相對該 step 正常輸出節奏明顯異常（CI extract 類場景通常秒級輸出、真空超過數分鐘即可疑）且最後一行是 happy log → silent hang 嫌疑高 → 用症狀詞查 upstream issue tracker，不是加 timeout。</p>
<p>三類 timeout 模式的修法不同：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>根因</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>進度持續、最後階段到 timeout</td>
          <td>時間真的不夠</td>
          <td>bump timeout</td>
      </tr>
      <tr>
          <td>有失敗訊息之後 timeout</td>
          <td>code 邏輯錯</td>
          <td>看訊息修</td>
      </tr>
      <tr>
          <td>最後一行 happy log 之後大段時間真空</td>
          <td>silent hang</td>
          <td>查 upstream issue tracker</td>
      </tr>
  </tbody>
</table>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>反覆加 timeout</strong>：每次都「差一點」（頂到上限），每次都以為「timeout 不夠」，實際上 process 永遠不會自己結束</li>
<li><strong>Cache 是假瓶頸</strong>：直覺判斷「下載慢 → 加 cache」，但瓶頸在 extract hang（下載只花 2 秒）</li>
<li><strong>False positive 越雕越精緻</strong>：cache key 調整、timeout 微調、retry 策略 — 每一步單看合理，合起來是把錯誤假設越做越細</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>兩個訊號同時出現時，應該先排除 silent hang 再提其他解法：</p>
<ol>
<li>非互動 process 跑的時間接近或等於 timeout 上限（「頂到上限」模式）</li>
<li>最後一行 log 是成功訊息（下載完成 / build succeeded / tests passed）</li>
</ol>
<p>另一個後設訊號：同方向修法（加 timeout / 加 cache / 加 retry）2 次都仍頂到上限 — 這時候問題幾乎確定不是「時間不夠」。對應 <a href="/blog/report/failure-direction-pivot-point/" data-link-title="同方向反覆失敗的轉折點" data-link-desc="第 2 次同方向失敗就停下來回報「假設可能錯了、要不要換思路」、不要等第 4 次失敗才被使用者打斷。本文展開失敗計數與方向切換的判斷。">#20 同方向反覆失敗的轉折點</a>。</p>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/failure-direction-pivot-point/" data-link-title="同方向反覆失敗的轉折點" data-link-desc="第 2 次同方向失敗就停下來回報「假設可能錯了、要不要換思路」、不要等第 4 次失敗才被使用者打斷。本文展開失敗計數與方向切換的判斷。">#20 同方向反覆失敗的轉折點</a>：本案例是 #20 在 CI timeout 場景的 evidence — 第二次 bump timeout 仍 fail 時就該停下來換思路</li>
<li>→ <a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a>：本卡的來源文章原本放在 <code>posts/</code>，實際是 debugging case study，搬到 <code>work-log/</code> 後從中抽出本卡，是 #199 拆分動作的實例</li>
</ul>
]]></content:encoded></item><item><title>Report 卡的論述基礎記結論和 evidence 來源、不記檢討過程</title><link>https://tarrragon.github.io/blog/report/report-basis-states-conclusion-not-process/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/report-basis-states-conclusion-not-process/</guid><description>&lt;h2 id="論述基礎與限制">論述基礎與限制&lt;/h2>
&lt;p>本卡抽自 #199 論述基礎段的修改。原稿寫了「四篇文章在寫作時自然累積了兩種內容、發表後讀起來沒頭沒尾不好讀、用 WRAP 完整模式分析後確認問題出在功能定位混合」— 包含發現問題的經過、使用的分析工具、主觀感受。修改後只留「本卡抽自四篇方法論文章同時塞 SOP 和驗證紀錄、導致兩種讀者都服務不好的分類檢討」。limitation：evidence 來自單一 report 卡的修改，但同模式在多張卡的論述基礎段出現過。&lt;/p>
&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>Report 卡的「論述基礎與限制」段承擔兩個功能：告訴讀者 evidence 從哪來（來源）、以及 evidence 的邊界是什麼（limitation）。不承擔第三個功能：告訴讀者這張卡怎麼被發起的（過程）。&lt;/p>
&lt;p>過程是作者的工作紀錄（「讀起來沒頭沒尾」「用 WRAP 分析」「用結構化決策分析」），結論才是讀者需要的判讀前提（「四篇文章混寫 SOP 和 retrospective」「evidence 來自同一個 blog」）。&lt;/p>
&lt;h2 id="情境">情境&lt;/h2>
&lt;p>#199 的論述基礎段原稿：&lt;/p>
&lt;blockquote>
&lt;p>四篇文章在寫作時自然累積了「怎麼做」（SOP）和「跑出來學到什麼」（retrospective）兩種內容，發表後讀起來「沒頭沒尾不好讀」。用結構化決策分析後確認：問題出在功能定位混合，跟寫作品質無關。&lt;/p>&lt;/blockquote>
&lt;p>三個問題：&lt;/p>
&lt;ol>
&lt;li>「自然累積了」「讀起來沒頭沒尾不好讀」是發起檢討的觸發描述、不是 evidence 邊界&lt;/li>
&lt;li>「用結構化決策分析後確認」是作者使用的工具、讀者不需要知道用了什麼工具才信任結論&lt;/li>
&lt;li>「跟寫作品質無關」是排除診斷的過程、結論段的「SOP 跟 retrospective 服務不同讀者」已經涵蓋&lt;/li>
&lt;/ol>
&lt;h2 id="理想做法">理想做法&lt;/h2>
&lt;p>論述基礎段只寫兩件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Evidence 來源&lt;/strong>：「本卡抽自 X」— X 是具體的 case / 事件 / 文章 / 批次&lt;/li>
&lt;li>&lt;strong>Evidence 邊界&lt;/strong>：「limitation：Y」— Y 是樣本量、適用範圍、已知盲區&lt;/li>
&lt;/ol>
&lt;p>不寫：&lt;/p>
&lt;ul>
&lt;li>怎麼發現問題的（「讀起來不好讀」「review 時發現」）&lt;/li>
&lt;li>用了什麼分析工具（「用 WRAP」「跑了三輪 review」）&lt;/li>
&lt;li>排除了什麼診斷（「跟 X 無關」）&lt;/li>
&lt;li>主觀感受（「沒頭沒尾」「覺得怪」）&lt;/li>
&lt;/ul>
&lt;h2 id="沒這樣做的麻煩">沒這樣做的麻煩&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>論述基礎段膨脹&lt;/strong>：過程描述比結論長，讀者要翻過「怎麼來的」才看到「邊界是什麼」&lt;/li>
&lt;li>&lt;strong>工具名耦合&lt;/strong>：寫了「用 WRAP 分析」，讀者不知道 WRAP 是什麼就卡住（冷讀者行話洩漏）&lt;/li>
&lt;li>&lt;strong>過時風險&lt;/strong>：過程描述綁定當時的工具和對話脈絡，卡片長期維護時過程段最先過時&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>寫完論述基礎段後，問兩個問題：&lt;/p>
&lt;ol>
&lt;li>「如果我把這段貼給一個沒參與這次檢討的人，他能從中知道 evidence 的來源和邊界嗎？」— 能 → 合格&lt;/li>
&lt;li>「這段有沒有只有參與者才懂的脈絡？」— 有 → 刪掉那些脈絡&lt;/li>
&lt;/ol>
&lt;h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能&lt;/a>：本卡的 trigger case；#199 論述基礎段的修改揭露此原則&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/summary-section-signals-scattered-prose/" data-link-title="教材的『重點 / 總結』段是內容發散的訊號、該重組正文不該補丁" data-link-desc="教材尾端的『重點』『一句話總結』段、若功能是重述前面已講過的內容、就是正文組織不佳的補丁。讀者需要回頭被提醒、代表概念在正文裡散掉了 —— 該做的是重拆正文段落、把概念在它該出現的位置一次講清、而不是另開總結段替發散的正文善後。判準：刪掉總結段後正文若仍站得住、證明總結本就冗餘；若站不住、是正文要重組、不是總結要補。處理總結段內容時先分『提醒 vs 概念』—— 純提醒（養成習慣 / 回頭確認）刪、有概念價值的（為何這樣設計）併回它本該所屬的正文位置。">#154 教材的重點/總結段是內容發散訊號&lt;/a>：#154 說「刪掉看正文站不站得住」，本卡的對應操作是「刪掉過程描述看論述基礎段站不站得住」&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/report/reader-does-not-need-to-know/" data-link-title="讀者不需要知道 — 刪除比解釋更尊重讀者" data-link-desc="「整理目的」blockquote 告訴讀者這篇文章的寫作動機和邊界。但讀者不需要知道作者為什麼寫這篇文章——他們需要知道讀完能帶走什麼。meta 資訊（寫作動機、邊界聲明、脈絡解釋）服務的是作者的組織需求，不是讀者的閱讀需求。">讀者不需要知道的資訊不該出現在最終文件&lt;/a>：過程描述是 meta 資訊、服務作者不服務讀者，同根原則在 report 卡 surface 的體現&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<h2 id="論述基礎與限制">論述基礎與限制</h2>
<p>本卡抽自 #199 論述基礎段的修改。原稿寫了「四篇文章在寫作時自然累積了兩種內容、發表後讀起來沒頭沒尾不好讀、用 WRAP 完整模式分析後確認問題出在功能定位混合」— 包含發現問題的經過、使用的分析工具、主觀感受。修改後只留「本卡抽自四篇方法論文章同時塞 SOP 和驗證紀錄、導致兩種讀者都服務不好的分類檢討」。limitation：evidence 來自單一 report 卡的修改，但同模式在多張卡的論述基礎段出現過。</p>
<h2 id="核心原則">核心原則</h2>
<p>Report 卡的「論述基礎與限制」段承擔兩個功能：告訴讀者 evidence 從哪來（來源）、以及 evidence 的邊界是什麼（limitation）。不承擔第三個功能：告訴讀者這張卡怎麼被發起的（過程）。</p>
<p>過程是作者的工作紀錄（「讀起來沒頭沒尾」「用 WRAP 分析」「用結構化決策分析」），結論才是讀者需要的判讀前提（「四篇文章混寫 SOP 和 retrospective」「evidence 來自同一個 blog」）。</p>
<h2 id="情境">情境</h2>
<p>#199 的論述基礎段原稿：</p>
<blockquote>
<p>四篇文章在寫作時自然累積了「怎麼做」（SOP）和「跑出來學到什麼」（retrospective）兩種內容，發表後讀起來「沒頭沒尾不好讀」。用結構化決策分析後確認：問題出在功能定位混合，跟寫作品質無關。</p></blockquote>
<p>三個問題：</p>
<ol>
<li>「自然累積了」「讀起來沒頭沒尾不好讀」是發起檢討的觸發描述、不是 evidence 邊界</li>
<li>「用結構化決策分析後確認」是作者使用的工具、讀者不需要知道用了什麼工具才信任結論</li>
<li>「跟寫作品質無關」是排除診斷的過程、結論段的「SOP 跟 retrospective 服務不同讀者」已經涵蓋</li>
</ol>
<h2 id="理想做法">理想做法</h2>
<p>論述基礎段只寫兩件事：</p>
<ol>
<li><strong>Evidence 來源</strong>：「本卡抽自 X」— X 是具體的 case / 事件 / 文章 / 批次</li>
<li><strong>Evidence 邊界</strong>：「limitation：Y」— Y 是樣本量、適用範圍、已知盲區</li>
</ol>
<p>不寫：</p>
<ul>
<li>怎麼發現問題的（「讀起來不好讀」「review 時發現」）</li>
<li>用了什麼分析工具（「用 WRAP」「跑了三輪 review」）</li>
<li>排除了什麼診斷（「跟 X 無關」）</li>
<li>主觀感受（「沒頭沒尾」「覺得怪」）</li>
</ul>
<h2 id="沒這樣做的麻煩">沒這樣做的麻煩</h2>
<ul>
<li><strong>論述基礎段膨脹</strong>：過程描述比結論長，讀者要翻過「怎麼來的」才看到「邊界是什麼」</li>
<li><strong>工具名耦合</strong>：寫了「用 WRAP 分析」，讀者不知道 WRAP 是什麼就卡住（冷讀者行話洩漏）</li>
<li><strong>過時風險</strong>：過程描述綁定當時的工具和對話脈絡，卡片長期維護時過程段最先過時</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>寫完論述基礎段後，問兩個問題：</p>
<ol>
<li>「如果我把這段貼給一個沒參與這次檢討的人，他能從中知道 evidence 的來源和邊界嗎？」— 能 → 合格</li>
<li>「這段有沒有只有參與者才懂的脈絡？」— 有 → 刪掉那些脈絡</li>
</ol>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<ul>
<li>→ <a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a>：本卡的 trigger case；#199 論述基礎段的修改揭露此原則</li>
<li>→ <a href="/blog/report/summary-section-signals-scattered-prose/" data-link-title="教材的『重點 / 總結』段是內容發散的訊號、該重組正文不該補丁" data-link-desc="教材尾端的『重點』『一句話總結』段、若功能是重述前面已講過的內容、就是正文組織不佳的補丁。讀者需要回頭被提醒、代表概念在正文裡散掉了 —— 該做的是重拆正文段落、把概念在它該出現的位置一次講清、而不是另開總結段替發散的正文善後。判準：刪掉總結段後正文若仍站得住、證明總結本就冗餘；若站不住、是正文要重組、不是總結要補。處理總結段內容時先分『提醒 vs 概念』—— 純提醒（養成習慣 / 回頭確認）刪、有概念價值的（為何這樣設計）併回它本該所屬的正文位置。">#154 教材的重點/總結段是內容發散訊號</a>：#154 說「刪掉看正文站不站得住」，本卡的對應操作是「刪掉過程描述看論述基礎段站不站得住」</li>
<li>→ <a href="/blog/report/reader-does-not-need-to-know/" data-link-title="讀者不需要知道 — 刪除比解釋更尊重讀者" data-link-desc="「整理目的」blockquote 告訴讀者這篇文章的寫作動機和邊界。但讀者不需要知道作者為什麼寫這篇文章——他們需要知道讀完能帶走什麼。meta 資訊（寫作動機、邊界聲明、脈絡解釋）服務的是作者的組織需求，不是讀者的閱讀需求。">讀者不需要知道的資訊不該出現在最終文件</a>：過程描述是 meta 資訊、服務作者不服務讀者，同根原則在 report 卡 surface 的體現</li>
</ul>
]]></content:encoded></item><item><title>多輪審查至少三輪是硬底線</title><link>https://tarrragon.github.io/blog/report/multi-round-review-minimum-three-rounds/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/multi-round-review-minimum-three-rounds/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>多輪審查（multi-round-review）的最低輪數是三輪，不是「看 finding 數決定要不要繼續」。Round 3 不是可選的加深，而是覆蓋 Round 1-2 結構性盲區的必要輪。&lt;/p>
&lt;h2 id="為什麼">為什麼&lt;/h2>
&lt;p>Round 1（compliance / baseline）和 Round 2（cadence / reader journey）用的 frame 都是「從作者端出發」的維度——規範有沒有遵守、句型有沒有重複、讀者走路線順不順。這兩輪能 catch 的問題有一個共同特徵：它們在「文章已經寫出來的內容」裡找錯。&lt;/p>
&lt;p>Round 3 的 frame 是「從文章沒寫的東西出發」——enumeration 有沒有漏選項（steelman）、其他系列有沒有反向引用（outbound）、搜尋落地粒度夠不夠（search landing）、知識卡缺口。這類問題在 Round 1-2 的 frame 下結構性不可見，因為 reviewer 在已有內容裡掃描時，不會主動問「這裡應該還有一個選項但沒寫」。&lt;/p>
&lt;h2 id="反模式">反模式&lt;/h2>
&lt;p>「Round 2 修完、finding 數下降、覺得差不多了就停」是最常見的反模式。multi-round-review skill 已經明確寫了「停止訊號是 frame 涵蓋、不是 finding 數遞減」，但實際執行時仍然會在 Round 2 結束後問「要不要繼續」——這個提問本身就是 finding 遞減直覺在主導判斷。&lt;/p>
&lt;h2 id="evidence">Evidence&lt;/h2>
&lt;p>Dotfile 系列（29 篇 + 知識卡）三輪審查的 finding 分布：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Round&lt;/th>
 &lt;th>Frame&lt;/th>
 &lt;th>Finding 數&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>規範 / fact-check / 一致性&lt;/td>
 &lt;td>15&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>Cadence / 讀者旅程 / 冷讀&lt;/td>
 &lt;td>14&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>Steelman / Outbound&lt;/td>
 &lt;td>14&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Round 3 的 14 項不是 Round 1-2 的殘餘——它們是全新類型的問題：macOS 原生 tiling 遺漏、yadm/mise 選項缺失、跨系列反向引用斷裂、知識卡缺口。這些問題在 Round 1-2 的 frame 下不會被 catch。&lt;/p>
&lt;p>先前的 backend 教學模組 review 也觀察到類似分布：三輪各 catch 不同類型的問題、finding 數不遞減。&lt;/p>
&lt;h2 id="修法">修法&lt;/h2>
&lt;p>把「至少三輪」從「建議」升級為「硬底線」。Round 3 結束後才進入「要不要繼續」的判讀——此時用七軸涵蓋度和「想不出新 frame」作為停止訊號。&lt;/p>
&lt;h2 id="跟其他原則的關係">跟其他原則的關係&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/writing-multi-pass-review/" data-link-title="Writing 的 multi-pass review：N 輪 review、每輪換 frame" data-link-desc="寫文章 / 註解 / 文件 / prompt 的「寫」不是單次動作 — 是 N 輪 review。第 1 輪生成、第 2 輪對意圖（#67）、第 3 輪檢查機會成本語氣、第 4 輪 grep-ability、第 5 輪反例 / 邊界。每輪不同 frame、單輪寫不出全部維度。本卡是 #82 在「寫」這個 output 動作的具體實例。">#114 multi-pass frame 顆粒度盲點&lt;/a> — 同 frame 多輪無增益，多輪價值在 frame 切換&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/cross-round-review-stopping-signal/" data-link-title="跨輪 review 停止訊號是 frame 涵蓋、不是 finding 數遞減" data-link-desc="判斷「該不該再來一輪 review」的訊號是『frame 軸是否還有未動』、不是『上一輪 finding 變少』；多輪 review 的 ROI 不是 monotonically decreasing、而是 frame 切換的質性轉換 — Round N 用新 frame 通常仍會抓出 substantial finding、但內容從 surface compliance 往深層 structural issue 走；停止訊號是「下一輪可用的新 frame 已經想不出來」、不是 finding 數遞減；本卡填補 #114 / #126 / #147 沒覆蓋的「何時夠了」判讀缺口">#148 跨輪 review 停止訊號&lt;/a> — 停止訊號是 frame 涵蓋、不是 finding 遞減&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/writing-review-multi-axis-completeness/" data-link-title="寫作 review 是多軸完整性、不是單軸深度" data-link-desc="寫作 review 的完整性不是單一軸越做越深、是多軸交集都對齊；#83 frame 軸 &amp;#43; #121 instance 軸 &amp;#43; #97 surface 軸 &amp;#43; #95 scope 軸 &amp;#43; #122 cadence 軸 &amp;#43; #124 timing 軸 &amp;#43; #114 granularity 軸、七軸正交、缺任一軸都會 systematic miss；review 設計時要 enumerate 七軸覆蓋狀況、不是只跑一兩個維度做深；是 #79 五維決策對話在 review 工具設計的姊妹卡">#126 review 七軸完整度&lt;/a> — 七軸動完是停止條件之一，三輪是動完七軸的最低路徑&lt;/li>
&lt;/ul>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;p>以下情境代表三輪硬底線正在被繞過：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>多輪審查（multi-round-review）的最低輪數是三輪，不是「看 finding 數決定要不要繼續」。Round 3 不是可選的加深，而是覆蓋 Round 1-2 結構性盲區的必要輪。</p>
<h2 id="為什麼">為什麼</h2>
<p>Round 1（compliance / baseline）和 Round 2（cadence / reader journey）用的 frame 都是「從作者端出發」的維度——規範有沒有遵守、句型有沒有重複、讀者走路線順不順。這兩輪能 catch 的問題有一個共同特徵：它們在「文章已經寫出來的內容」裡找錯。</p>
<p>Round 3 的 frame 是「從文章沒寫的東西出發」——enumeration 有沒有漏選項（steelman）、其他系列有沒有反向引用（outbound）、搜尋落地粒度夠不夠（search landing）、知識卡缺口。這類問題在 Round 1-2 的 frame 下結構性不可見，因為 reviewer 在已有內容裡掃描時，不會主動問「這裡應該還有一個選項但沒寫」。</p>
<h2 id="反模式">反模式</h2>
<p>「Round 2 修完、finding 數下降、覺得差不多了就停」是最常見的反模式。multi-round-review skill 已經明確寫了「停止訊號是 frame 涵蓋、不是 finding 數遞減」，但實際執行時仍然會在 Round 2 結束後問「要不要繼續」——這個提問本身就是 finding 遞減直覺在主導判斷。</p>
<h2 id="evidence">Evidence</h2>
<p>Dotfile 系列（29 篇 + 知識卡）三輪審查的 finding 分布：</p>
<table>
  <thead>
      <tr>
          <th>Round</th>
          <th>Frame</th>
          <th>Finding 數</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>規範 / fact-check / 一致性</td>
          <td>15</td>
      </tr>
      <tr>
          <td>2</td>
          <td>Cadence / 讀者旅程 / 冷讀</td>
          <td>14</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Steelman / Outbound</td>
          <td>14</td>
      </tr>
  </tbody>
</table>
<p>Round 3 的 14 項不是 Round 1-2 的殘餘——它們是全新類型的問題：macOS 原生 tiling 遺漏、yadm/mise 選項缺失、跨系列反向引用斷裂、知識卡缺口。這些問題在 Round 1-2 的 frame 下不會被 catch。</p>
<p>先前的 backend 教學模組 review 也觀察到類似分布：三輪各 catch 不同類型的問題、finding 數不遞減。</p>
<h2 id="修法">修法</h2>
<p>把「至少三輪」從「建議」升級為「硬底線」。Round 3 結束後才進入「要不要繼續」的判讀——此時用七軸涵蓋度和「想不出新 frame」作為停止訊號。</p>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<ul>
<li><a href="/blog/report/writing-multi-pass-review/" data-link-title="Writing 的 multi-pass review：N 輪 review、每輪換 frame" data-link-desc="寫文章 / 註解 / 文件 / prompt 的「寫」不是單次動作 — 是 N 輪 review。第 1 輪生成、第 2 輪對意圖（#67）、第 3 輪檢查機會成本語氣、第 4 輪 grep-ability、第 5 輪反例 / 邊界。每輪不同 frame、單輪寫不出全部維度。本卡是 #82 在「寫」這個 output 動作的具體實例。">#114 multi-pass frame 顆粒度盲點</a> — 同 frame 多輪無增益，多輪價值在 frame 切換</li>
<li><a href="/blog/report/cross-round-review-stopping-signal/" data-link-title="跨輪 review 停止訊號是 frame 涵蓋、不是 finding 數遞減" data-link-desc="判斷「該不該再來一輪 review」的訊號是『frame 軸是否還有未動』、不是『上一輪 finding 變少』；多輪 review 的 ROI 不是 monotonically decreasing、而是 frame 切換的質性轉換 — Round N 用新 frame 通常仍會抓出 substantial finding、但內容從 surface compliance 往深層 structural issue 走；停止訊號是「下一輪可用的新 frame 已經想不出來」、不是 finding 數遞減；本卡填補 #114 / #126 / #147 沒覆蓋的「何時夠了」判讀缺口">#148 跨輪 review 停止訊號</a> — 停止訊號是 frame 涵蓋、不是 finding 遞減</li>
<li><a href="/blog/report/writing-review-multi-axis-completeness/" data-link-title="寫作 review 是多軸完整性、不是單軸深度" data-link-desc="寫作 review 的完整性不是單一軸越做越深、是多軸交集都對齊；#83 frame 軸 &#43; #121 instance 軸 &#43; #97 surface 軸 &#43; #95 scope 軸 &#43; #122 cadence 軸 &#43; #124 timing 軸 &#43; #114 granularity 軸、七軸正交、缺任一軸都會 systematic miss；review 設計時要 enumerate 七軸覆蓋狀況、不是只跑一兩個維度做深；是 #79 五維決策對話在 review 工具設計的姊妹卡">#126 review 七軸完整度</a> — 七軸動完是停止條件之一，三輪是動完七軸的最低路徑</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>以下情境代表三輪硬底線正在被繞過：</p>
<ul>
<li>Round 2 結束後問「要不要繼續」「到這裡收嗎」</li>
<li>Round 3 的 frame 規劃被跳過、直接宣布 review 完成</li>
<li>用「Round 2 finding 數比 Round 1 少」作為停止依據</li>
</ul>
]]></content:encoded></item><item><title>三輪審查的檢討收穫 — 工具 opinion 文章的寫作品質演進</title><link>https://tarrragon.github.io/blog/report/tool-opinion-article-review-lessons/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/tool-opinion-article-review-lessons/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>多輪審查的價值在 frame 切換而非重複加深。三輪用了 8 個不同 frame，每輪抓到的問題類型和上一輪不重疊。停止訊號是「想不出新 frame」而非「finding 數遞減」。&lt;/p>
&lt;h2 id="每輪的-frame-和收穫">每輪的 frame 和收穫&lt;/h2>
&lt;h3 id="round-1compliance規範--事實--冷讀">Round 1：Compliance（規範 + 事實 + 冷讀）&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Frame&lt;/th>
 &lt;th>代表 finding&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫作規範&lt;/td>
 &lt;td>「唯一」必然性框架（3 處）、meta-commentary 外露&lt;/td>
 &lt;td>字句層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Fact-check&lt;/td>
 &lt;td>semver 表格混用專案術語和標準定義&lt;/td>
 &lt;td>準確性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>冷讀零脈絡&lt;/td>
 &lt;td>「Proposal」「Wave」行話洩漏、缺進入動機&lt;/td>
 &lt;td>可讀性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="round-2cadence--讀者旅程--title-commitment">Round 2：Cadence + 讀者旅程 + Title commitment&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Frame&lt;/th>
 &lt;th>代表 finding&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Cadence&lt;/td>
 &lt;td>「為什麼會這樣」兩小節骨架完全複製&lt;/td>
 &lt;td>結構層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reader simulation&lt;/td>
 &lt;td>事件段佔比高、原則到得晚、CoC 結尾像附錄&lt;/td>
 &lt;td>節奏層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Title commitment&lt;/td>
 &lt;td>title 承諾「為什麼」但 L86 才正式交付&lt;/td>
 &lt;td>對齊層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="round-3self-application--steelman">Round 3：Self-application + Steelman&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Frame&lt;/th>
 &lt;th>代表 finding&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Self-application&lt;/td>
 &lt;td>文章主張「用結構引導」但自己的標題路徑偏事件&lt;/td>
 &lt;td>Meta 層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Steelman&lt;/td>
 &lt;td>缺 soft/hard opinion 區分、缺 cost-benefit 不對稱論證&lt;/td>
 &lt;td>論證層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="三輪漏抓鏈為什麼前一輪漏了">三輪漏抓鏈：為什麼前一輪漏了&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>同骨化（兩段結構複製）&lt;/td>
 &lt;td>R2&lt;/td>
 &lt;td>R1 逐段掃描，同骨化是跨段比對才浮現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CoC 結尾收束弱&lt;/td>
 &lt;td>R2&lt;/td>
 &lt;td>R1 檢查的是每段內容是否合規，不評估段落在全文中的位置價值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>缺 soft/hard opinion 區分&lt;/td>
 &lt;td>R3&lt;/td>
 &lt;td>R1-R2 的讀者角色是「初次讀者」，R3 的讀者角色是「知識淵博的反駁者」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文章未 self-apply 自己的主張&lt;/td>
 &lt;td>R3&lt;/td>
 &lt;td>R1-R2 把文章當內容審查，R3 把文章當主張並用主張反向檢查&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每一輪的遺漏都是因為 frame 的視角限制——同一個 reviewer 用同一個 frame 跑多輪只會重複相同的 catch。多輪的價值在 frame 切換。&lt;/p>
&lt;h2 id="你的文章需要多輪審查嗎">你的文章需要多輪審查嗎？&lt;/h2>
&lt;ul>
&lt;li>品質敏感（教學、規範、長期累積的內容）且篇幅 &amp;gt; 100 行 → 至少 2 輪不同 frame&lt;/li>
&lt;li>第一輪 finding &amp;gt; 10 → 再加一輪&lt;/li>
&lt;li>同一批次有 3+ 篇相關文章 → 加跨篇一致性 frame&lt;/li>
&lt;li>想不出新 frame → 停止&lt;/li>
&lt;/ul>
&lt;h2 id="ai-輔助寫作中反覆出現的-pattern">AI 輔助寫作中反覆出現的 pattern&lt;/h2>
&lt;p>從這次和過去的審查經驗，以下 pattern 在 AI 生成的文章中出現頻率高：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pattern&lt;/th>
 &lt;th>出現頻率&lt;/th>
 &lt;th>為什麼 AI 容易犯&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>必然性框架（「唯一」「天生」「本質」）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>AI 傾向用強勢措辭增加說服力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>負向開頭（「不是 X」「沒有 Y」）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>否定句是 AI 常用的對比修辭&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>meta-commentary（「先交代脈絡…」）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>AI 把內部推理過程外露到文章中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同骨化&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>同一 prompt 生成的多段文字共用隱含模板&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>結尾重複前文&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>AI 傾向在結尾摘要前文而非昇華&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些 pattern 的防護適合放在生成端（寫作前的 checklist），而非只放在審查端（寫完後掃描）。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>多輪審查的價值在 frame 切換而非重複加深。三輪用了 8 個不同 frame，每輪抓到的問題類型和上一輪不重疊。停止訊號是「想不出新 frame」而非「finding 數遞減」。</p>
<h2 id="每輪的-frame-和收穫">每輪的 frame 和收穫</h2>
<h3 id="round-1compliance規範--事實--冷讀">Round 1：Compliance（規範 + 事實 + 冷讀）</h3>
<table>
  <thead>
      <tr>
          <th>Frame</th>
          <th>代表 finding</th>
          <th>類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫作規範</td>
          <td>「唯一」必然性框架（3 處）、meta-commentary 外露</td>
          <td>字句層</td>
      </tr>
      <tr>
          <td>Fact-check</td>
          <td>semver 表格混用專案術語和標準定義</td>
          <td>準確性</td>
      </tr>
      <tr>
          <td>冷讀零脈絡</td>
          <td>「Proposal」「Wave」行話洩漏、缺進入動機</td>
          <td>可讀性</td>
      </tr>
  </tbody>
</table>
<h3 id="round-2cadence--讀者旅程--title-commitment">Round 2：Cadence + 讀者旅程 + Title commitment</h3>
<table>
  <thead>
      <tr>
          <th>Frame</th>
          <th>代表 finding</th>
          <th>類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cadence</td>
          <td>「為什麼會這樣」兩小節骨架完全複製</td>
          <td>結構層</td>
      </tr>
      <tr>
          <td>Reader simulation</td>
          <td>事件段佔比高、原則到得晚、CoC 結尾像附錄</td>
          <td>節奏層</td>
      </tr>
      <tr>
          <td>Title commitment</td>
          <td>title 承諾「為什麼」但 L86 才正式交付</td>
          <td>對齊層</td>
      </tr>
  </tbody>
</table>
<h3 id="round-3self-application--steelman">Round 3：Self-application + Steelman</h3>
<table>
  <thead>
      <tr>
          <th>Frame</th>
          <th>代表 finding</th>
          <th>類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Self-application</td>
          <td>文章主張「用結構引導」但自己的標題路徑偏事件</td>
          <td>Meta 層</td>
      </tr>
      <tr>
          <td>Steelman</td>
          <td>缺 soft/hard opinion 區分、缺 cost-benefit 不對稱論證</td>
          <td>論證層</td>
      </tr>
  </tbody>
</table>
<h2 id="三輪漏抓鏈為什麼前一輪漏了">三輪漏抓鏈：為什麼前一輪漏了</h2>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>哪一輪抓到</th>
          <th>為什麼前面漏了</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同骨化（兩段結構複製）</td>
          <td>R2</td>
          <td>R1 逐段掃描，同骨化是跨段比對才浮現</td>
      </tr>
      <tr>
          <td>CoC 結尾收束弱</td>
          <td>R2</td>
          <td>R1 檢查的是每段內容是否合規，不評估段落在全文中的位置價值</td>
      </tr>
      <tr>
          <td>缺 soft/hard opinion 區分</td>
          <td>R3</td>
          <td>R1-R2 的讀者角色是「初次讀者」，R3 的讀者角色是「知識淵博的反駁者」</td>
      </tr>
      <tr>
          <td>文章未 self-apply 自己的主張</td>
          <td>R3</td>
          <td>R1-R2 把文章當內容審查，R3 把文章當主張並用主張反向檢查</td>
      </tr>
  </tbody>
</table>
<p>每一輪的遺漏都是因為 frame 的視角限制——同一個 reviewer 用同一個 frame 跑多輪只會重複相同的 catch。多輪的價值在 frame 切換。</p>
<h2 id="你的文章需要多輪審查嗎">你的文章需要多輪審查嗎？</h2>
<ul>
<li>品質敏感（教學、規範、長期累積的內容）且篇幅 &gt; 100 行 → 至少 2 輪不同 frame</li>
<li>第一輪 finding &gt; 10 → 再加一輪</li>
<li>同一批次有 3+ 篇相關文章 → 加跨篇一致性 frame</li>
<li>想不出新 frame → 停止</li>
</ul>
<h2 id="ai-輔助寫作中反覆出現的-pattern">AI 輔助寫作中反覆出現的 pattern</h2>
<p>從這次和過去的審查經驗，以下 pattern 在 AI 生成的文章中出現頻率高：</p>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>出現頻率</th>
          <th>為什麼 AI 容易犯</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>必然性框架（「唯一」「天生」「本質」）</td>
          <td>高</td>
          <td>AI 傾向用強勢措辭增加說服力</td>
      </tr>
      <tr>
          <td>負向開頭（「不是 X」「沒有 Y」）</td>
          <td>高</td>
          <td>否定句是 AI 常用的對比修辭</td>
      </tr>
      <tr>
          <td>meta-commentary（「先交代脈絡…」）</td>
          <td>中</td>
          <td>AI 把內部推理過程外露到文章中</td>
      </tr>
      <tr>
          <td>同骨化</td>
          <td>高</td>
          <td>同一 prompt 生成的多段文字共用隱含模板</td>
      </tr>
      <tr>
          <td>結尾重複前文</td>
          <td>中</td>
          <td>AI 傾向在結尾摘要前文而非昇華</td>
      </tr>
  </tbody>
</table>
<p>這些 pattern 的防護適合放在生成端（寫作前的 checklist），而非只放在審查端（寫完後掃描）。</p>
]]></content:encoded></item><item><title>文章語氣校正：恐嚇式 hook 與技術分享的邊界</title><link>https://tarrragon.github.io/blog/report/article-tone-scare-vs-share/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/article-tone-scare-vs-share/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>技術文章的開頭語氣決定了讀者和作者的關係。恐嚇式語氣（「問題可能正在發生」）把讀者放在「被警告的對象」位置；分享式語氣（「從一個經驗出發討論」）把讀者放在「同行」位置。兩者傳遞相同的資訊，但讀者的接收姿態不同。&lt;/p>
&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>「這篇討論的 pattern 可能正在你的工具中靜默發生」&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;/tbody>
&lt;/table>
&lt;p>兩句話的資訊含量相同（都在說「這篇討論工具設計的某個面向」）。差異在語氣詞：「靜默發生」暗示讀者有問題但不知道，帶有居高臨下的意味；「容易忽略的面向」承認這是普遍現象，不預設讀者有問題。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>寫完開頭後問一個問題：&lt;strong>如果把「你」換成「我們」，語句是否仍然自然？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>「可能正在&lt;strong>我們的&lt;/strong>工具中靜默發生」——可以，但語氣變成自我診斷，不像是要嚇別人&lt;/li>
&lt;li>「從一個經驗出發，討論&lt;strong>我們&lt;/strong>容易忽略的面向」——自然，分享語氣不變&lt;/li>
&lt;/ul>
&lt;p>如果換成「我們」後語氣彆扭，代表原句預設了作者和讀者的不對稱——作者知道問題、讀者不知道。技術分享的前提是對稱：我遇到了，你可能也會遇到，我們一起看看。&lt;/p>
&lt;h2 id="ai-寫作的傾向">AI 寫作的傾向&lt;/h2>
&lt;p>AI 生成的開頭容易偏向恐嚇式，因為訓練資料中大量的技術文章用「你可能不知道的 N 件事」「你一直在犯的 N 個錯誤」作為 hook。這些 hook 在點擊率導向的平台上有效，但在技術社群中會降低信任——讀者會覺得「你在教我做事」。&lt;/p>
&lt;p>生成端防護：寫完 hook 後檢查有沒有「正在」「靜默」「不知不覺」「你可能」等暗示讀者無知的措辭。&lt;/p>
&lt;p>&lt;strong>場景邊界&lt;/strong>：此判斷標準適用於經驗分享和檢討文章。教學文章的第二人稱指引（「你應該先安裝 X」）屬合理用法——教學的語氣本就是引導式。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>技術文章的開頭語氣決定了讀者和作者的關係。恐嚇式語氣（「問題可能正在發生」）把讀者放在「被警告的對象」位置；分享式語氣（「從一個經驗出發討論」）把讀者放在「同行」位置。兩者傳遞相同的資訊，但讀者的接收姿態不同。</p>
<h2 id="案例">案例</h2>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>語句</th>
          <th>語氣</th>
          <th>讀者位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>初稿</td>
          <td>「這篇討論的 pattern 可能正在你的工具中靜默發生」</td>
          <td>恐嚇</td>
          <td>被警告者</td>
      </tr>
      <tr>
          <td>修正</td>
          <td>「這篇從一個版本錯置的經驗出發，討論工具設計中一個容易忽略的面向」</td>
          <td>分享</td>
          <td>同行</td>
      </tr>
  </tbody>
</table>
<p>兩句話的資訊含量相同（都在說「這篇討論工具設計的某個面向」）。差異在語氣詞：「靜默發生」暗示讀者有問題但不知道，帶有居高臨下的意味；「容易忽略的面向」承認這是普遍現象，不預設讀者有問題。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>寫完開頭後問一個問題：<strong>如果把「你」換成「我們」，語句是否仍然自然？</strong></p>
<ul>
<li>「可能正在<strong>我們的</strong>工具中靜默發生」——可以，但語氣變成自我診斷，不像是要嚇別人</li>
<li>「從一個經驗出發，討論<strong>我們</strong>容易忽略的面向」——自然，分享語氣不變</li>
</ul>
<p>如果換成「我們」後語氣彆扭，代表原句預設了作者和讀者的不對稱——作者知道問題、讀者不知道。技術分享的前提是對稱：我遇到了，你可能也會遇到，我們一起看看。</p>
<h2 id="ai-寫作的傾向">AI 寫作的傾向</h2>
<p>AI 生成的開頭容易偏向恐嚇式，因為訓練資料中大量的技術文章用「你可能不知道的 N 件事」「你一直在犯的 N 個錯誤」作為 hook。這些 hook 在點擊率導向的平台上有效，但在技術社群中會降低信任——讀者會覺得「你在教我做事」。</p>
<p>生成端防護：寫完 hook 後檢查有沒有「正在」「靜默」「不知不覺」「你可能」等暗示讀者無知的措辭。</p>
<p><strong>場景邊界</strong>：此判斷標準適用於經驗分享和檢討文章。教學文章的第二人稱指引（「你應該先安裝 X」）屬合理用法——教學的語氣本就是引導式。</p>
]]></content:encoded></item><item><title>文章範圍漂移：從 CLI 工具到工具設計的泛化過程</title><link>https://tarrragon.github.io/blog/report/article-scope-creep-cli-to-tool-design/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/article-scope-creep-cli-to-tool-design/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>文章範圍的泛化（CLI → 工具設計）需要在五個位置同步調整，遺漏任一個都會讓文章的「說的」和「做的」不一致：title、開頭 hook、原則段措辭、對照表範例、結尾 checklist。&lt;/p>
&lt;h2 id="漂移軌跡">漂移軌跡&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>初稿&lt;/td>
 &lt;td>CLI 工具的 opinion&lt;/td>
 &lt;td>從 ticket CLI 事件出發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>R1 修法後&lt;/td>
 &lt;td>CLI 工具，但原則段已泛化&lt;/td>
 &lt;td>審查建議補遷移性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>作者第一次回饋&lt;/td>
 &lt;td>「不應該侷限於 CLI」&lt;/td>
 &lt;td>作者指出核心觀點適用於所有工具&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>泛化修正&lt;/td>
 &lt;td>所有工具設計&lt;/td>
 &lt;td>調整 title / hook / 原則 / checklist / 對照表&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>作者第二次回饋&lt;/td>
 &lt;td>語氣從指令式改為分享式&lt;/td>
 &lt;td>title 中「你的 CLI 應該有 opinion」太喊話&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>&lt;strong>案例具體、原則泛化&lt;/strong>是 work-log 的理想結構：用具體的 CLI 事件作為案例（讀者需要具體故事才能共鳴），但原則段和 checklist 不綁定在案例的技術棧上（讀者需要帶走可遷移的 takeaway）。&lt;/p>
&lt;p>泛化時容易遺漏的是&lt;strong>中間地帶&lt;/strong>——title 和 hook 已經泛化，但內文的某句話仍寫著「Opinionated CLI」或「列出你的 CLI 所有參數」。這類殘留在自己反覆讀時不容易發現（因為知道意思），但冷讀者會注意到 title 說「工具設計」而內文只講 CLI。&lt;/p>
&lt;p>逐行 grep 原始範圍的關鍵詞（如 &lt;code>CLI&lt;/code>、&lt;code>命令列&lt;/code>）是最有效的殘留偵測方式。每處命中判斷：是案例引用（保留）還是原則陳述（改泛化）。&lt;/p>
&lt;p>這個 pattern 適用於任何「把具體經驗提煉為通用原則」的寫作——技術 blog、內部 postmortem、教學文章。泛化的時機通常在第一輪回饋後，觸發點是讀者說「這不只適用於你的場景」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>文章範圍的泛化（CLI → 工具設計）需要在五個位置同步調整，遺漏任一個都會讓文章的「說的」和「做的」不一致：title、開頭 hook、原則段措辭、對照表範例、結尾 checklist。</p>
<h2 id="漂移軌跡">漂移軌跡</h2>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>範圍</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>初稿</td>
          <td>CLI 工具的 opinion</td>
          <td>從 ticket CLI 事件出發</td>
      </tr>
      <tr>
          <td>R1 修法後</td>
          <td>CLI 工具，但原則段已泛化</td>
          <td>審查建議補遷移性</td>
      </tr>
      <tr>
          <td>作者第一次回饋</td>
          <td>「不應該侷限於 CLI」</td>
          <td>作者指出核心觀點適用於所有工具</td>
      </tr>
      <tr>
          <td>泛化修正</td>
          <td>所有工具設計</td>
          <td>調整 title / hook / 原則 / checklist / 對照表</td>
      </tr>
      <tr>
          <td>作者第二次回饋</td>
          <td>語氣從指令式改為分享式</td>
          <td>title 中「你的 CLI 應該有 opinion」太喊話</td>
      </tr>
  </tbody>
</table>
<h2 id="教訓">教訓</h2>
<p><strong>案例具體、原則泛化</strong>是 work-log 的理想結構：用具體的 CLI 事件作為案例（讀者需要具體故事才能共鳴），但原則段和 checklist 不綁定在案例的技術棧上（讀者需要帶走可遷移的 takeaway）。</p>
<p>泛化時容易遺漏的是<strong>中間地帶</strong>——title 和 hook 已經泛化，但內文的某句話仍寫著「Opinionated CLI」或「列出你的 CLI 所有參數」。這類殘留在自己反覆讀時不容易發現（因為知道意思），但冷讀者會注意到 title 說「工具設計」而內文只講 CLI。</p>
<p>逐行 grep 原始範圍的關鍵詞（如 <code>CLI</code>、<code>命令列</code>）是最有效的殘留偵測方式。每處命中判斷：是案例引用（保留）還是原則陳述（改泛化）。</p>
<p>這個 pattern 適用於任何「把具體經驗提煉為通用原則」的寫作——技術 blog、內部 postmortem、教學文章。泛化的時機通常在第一輪回饋後，觸發點是讀者說「這不只適用於你的場景」。</p>
]]></content:encoded></item><item><title>主題偏移：內部系統知識洩漏到面向讀者的論述</title><link>https://tarrragon.github.io/blog/report/topic-drift-internal-knowledge-leak/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/topic-drift-internal-knowledge-leak/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>當一句話在技術上正確但偏離文章主題時，刪除比保留好。補充內部系統的細節滿足的是作者的完整性需求，不是讀者的理解需求。&lt;/p>
&lt;h2 id="案例">案例&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>修正前&lt;/td>
 &lt;td>「opinion 是最可靠的防線。&lt;strong>規則檔和 memory 系統也能傳遞教訓，但工具在操作當下的即時引導是離決策點最近的攔截——規則是知識層，工具是執行層，兩者互補但執行層更難繞過&lt;/strong>」&lt;/td>
 &lt;td>粗體部分展開了作者專案的內部架構（規則檔 / memory / 知識層 / 執行層），偏離了「工具應該有 opinion」的主題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修正後&lt;/td>
 &lt;td>「opinion 是最可靠的防線，因為工具在操作當下的即時引導是離決策點最近的攔截」&lt;/td>
 &lt;td>保留核心論點，刪除內部系統細節&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>修正前的語句在技術上完全正確——規則檔和 memory 系統確實是防線的一部分。但這個補充回答的是「作者的系統有哪些防護層」，而不是「為什麼工具 opinion 重要」。讀者來這篇文章是想知道後者。&lt;/p>
&lt;h2 id="判斷流程">判斷流程&lt;/h2>
&lt;p>對每段補充依序判斷：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>主題相關性&lt;/strong>：這段話回答的是文章標題承諾的問題，還是一個衍生的子問題？&lt;/li>
&lt;li>&lt;strong>刪除測試&lt;/strong>：刪掉這段話，讀者對主題的理解會減少嗎？&lt;/li>
&lt;li>&lt;strong>受眾測試&lt;/strong>：這段話的受眾是讀者（幫助他理解主題），還是作者（滿足自己的完整性偏好）？&lt;/li>
&lt;/ol>
&lt;p>三者任一指向「不服務主題/讀者」→ 刪除。「規則檔和 memory 系統」的補充在三項測試中都指向刪除：它回答的是「作者的系統有哪些防護層」（衍生問題）、刪掉不影響理解、受眾是作者。&lt;/p>
&lt;h2 id="延伸">延伸&lt;/h2>
&lt;p>AI 生成的文章容易出現這類偏移——prompt context 中的系統知識會被 AI 當作「相關因此應寫入」。但「推理中相關」和「讀者需要知道」是兩件事。同類問題（AI 把內部推理殘留外露到文章中）的另一面見 &lt;a href="https://tarrragon.github.io/blog/report/reader-does-not-need-to-know/" data-link-title="讀者不需要知道 — 刪除比解釋更尊重讀者" data-link-desc="「整理目的」blockquote 告訴讀者這篇文章的寫作動機和邊界。但讀者不需要知道作者為什麼寫這篇文章——他們需要知道讀完能帶走什麼。meta 資訊（寫作動機、邊界聲明、脈絡解釋）服務的是作者的組織需求，不是讀者的閱讀需求。">reader-does-not-need-to-know&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>當一句話在技術上正確但偏離文章主題時，刪除比保留好。補充內部系統的細節滿足的是作者的完整性需求，不是讀者的理解需求。</p>
<h2 id="案例">案例</h2>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>語句</th>
          <th>問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修正前</td>
          <td>「opinion 是最可靠的防線。<strong>規則檔和 memory 系統也能傳遞教訓，但工具在操作當下的即時引導是離決策點最近的攔截——規則是知識層，工具是執行層，兩者互補但執行層更難繞過</strong>」</td>
          <td>粗體部分展開了作者專案的內部架構（規則檔 / memory / 知識層 / 執行層），偏離了「工具應該有 opinion」的主題</td>
      </tr>
      <tr>
          <td>修正後</td>
          <td>「opinion 是最可靠的防線，因為工具在操作當下的即時引導是離決策點最近的攔截」</td>
          <td>保留核心論點，刪除內部系統細節</td>
      </tr>
  </tbody>
</table>
<p>修正前的語句在技術上完全正確——規則檔和 memory 系統確實是防線的一部分。但這個補充回答的是「作者的系統有哪些防護層」，而不是「為什麼工具 opinion 重要」。讀者來這篇文章是想知道後者。</p>
<h2 id="判斷流程">判斷流程</h2>
<p>對每段補充依序判斷：</p>
<ol>
<li><strong>主題相關性</strong>：這段話回答的是文章標題承諾的問題，還是一個衍生的子問題？</li>
<li><strong>刪除測試</strong>：刪掉這段話，讀者對主題的理解會減少嗎？</li>
<li><strong>受眾測試</strong>：這段話的受眾是讀者（幫助他理解主題），還是作者（滿足自己的完整性偏好）？</li>
</ol>
<p>三者任一指向「不服務主題/讀者」→ 刪除。「規則檔和 memory 系統」的補充在三項測試中都指向刪除：它回答的是「作者的系統有哪些防護層」（衍生問題）、刪掉不影響理解、受眾是作者。</p>
<h2 id="延伸">延伸</h2>
<p>AI 生成的文章容易出現這類偏移——prompt context 中的系統知識會被 AI 當作「相關因此應寫入」。但「推理中相關」和「讀者需要知道」是兩件事。同類問題（AI 把內部推理殘留外露到文章中）的另一面見 <a href="/blog/report/reader-does-not-need-to-know/" data-link-title="讀者不需要知道 — 刪除比解釋更尊重讀者" data-link-desc="「整理目的」blockquote 告訴讀者這篇文章的寫作動機和邊界。但讀者不需要知道作者為什麼寫這篇文章——他們需要知道讀完能帶走什麼。meta 資訊（寫作動機、邊界聲明、脈絡解釋）服務的是作者的組織需求，不是讀者的閱讀需求。">reader-does-not-need-to-know</a>。</p>
]]></content:encoded></item><item><title>信號不是承認 — 技術寫作中的歸因語氣</title><link>https://tarrragon.github.io/blog/report/signal-not-admission/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/signal-not-admission/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>描述系統行為時，使用「信號」「提醒」「觀測」等中性詞，避免「承認」「暴露」「證明了失敗」等歸因詞。工程上的信號是指向可改善之處的指標，不是對過去決策的道德判定。&lt;/p>
&lt;h2 id="案例">案例&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>修正前&lt;/td>
 &lt;td>「每一個『寫文件提醒』都是&lt;strong>承認&lt;/strong>『工具沒做到位』的信號」&lt;/td>
 &lt;td>「承認」是歸因詞——暗示設計者做錯了、需要認錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修正後&lt;/td>
 &lt;td>「每一個『寫文件提醒』都是一個&lt;strong>信號&lt;/strong>——工具的預設行為還有空間改善」&lt;/td>
 &lt;td>「信號」是觀測詞——指向改善方向，不判定對錯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>兩句話指向相同的行動（評估能否把提醒轉化為工具預設行為），但讀者的接收姿態不同。「承認」讓讀者進入防禦模式（「我的工具沒做錯，文件有文件的用途」）；「信號」讓讀者進入改善模式（「確實，這個文件提醒可以被工具化」）。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>寫完一句帶有歸因意味的描述後，問：&lt;strong>這句話在描述事實，還是在分配責任？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>事實：「文件提醒的存在表示工具的預設行為有改善空間」&lt;/li>
&lt;li>歸因：「文件提醒的存在承認了工具沒做到位」&lt;/li>
&lt;/ul>
&lt;p>如果描述的目的是引導讀者改善，用事實語氣。歸因語氣適合事故報告（需要追溯責任），不適合技術分享（目的是傳遞改善思路）。&lt;/p>
&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;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>承認&lt;/td>
 &lt;td>表示、反映&lt;/td>
 &lt;td>「承認」有認錯語意&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>暴露了&lt;/td>
 &lt;td>顯示出&lt;/td>
 &lt;td>「暴露」暗示刻意隱藏&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>證明了失敗&lt;/td>
 &lt;td>顯示有改善空間&lt;/td>
 &lt;td>「失敗」是終結判定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>被迫&lt;/td>
 &lt;td>選擇、改為&lt;/td>
 &lt;td>「被迫」暗示無奈而非決策（描述外部強制約束時可保留）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>描述系統行為時，使用「信號」「提醒」「觀測」等中性詞，避免「承認」「暴露」「證明了失敗」等歸因詞。工程上的信號是指向可改善之處的指標，不是對過去決策的道德判定。</p>
<h2 id="案例">案例</h2>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>語句</th>
          <th>問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修正前</td>
          <td>「每一個『寫文件提醒』都是<strong>承認</strong>『工具沒做到位』的信號」</td>
          <td>「承認」是歸因詞——暗示設計者做錯了、需要認錯</td>
      </tr>
      <tr>
          <td>修正後</td>
          <td>「每一個『寫文件提醒』都是一個<strong>信號</strong>——工具的預設行為還有空間改善」</td>
          <td>「信號」是觀測詞——指向改善方向，不判定對錯</td>
      </tr>
  </tbody>
</table>
<p>兩句話指向相同的行動（評估能否把提醒轉化為工具預設行為），但讀者的接收姿態不同。「承認」讓讀者進入防禦模式（「我的工具沒做錯，文件有文件的用途」）；「信號」讓讀者進入改善模式（「確實，這個文件提醒可以被工具化」）。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>寫完一句帶有歸因意味的描述後，問：<strong>這句話在描述事實，還是在分配責任？</strong></p>
<ul>
<li>事實：「文件提醒的存在表示工具的預設行為有改善空間」</li>
<li>歸因：「文件提醒的存在承認了工具沒做到位」</li>
</ul>
<p>如果描述的目的是引導讀者改善，用事實語氣。歸因語氣適合事故報告（需要追溯責任），不適合技術分享（目的是傳遞改善思路）。</p>
<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>暴露了</td>
          <td>顯示出</td>
          <td>「暴露」暗示刻意隱藏</td>
      </tr>
      <tr>
          <td>證明了失敗</td>
          <td>顯示有改善空間</td>
          <td>「失敗」是終結判定</td>
      </tr>
      <tr>
          <td>被迫</td>
          <td>選擇、改為</td>
          <td>「被迫」暗示無奈而非決策（描述外部強制約束時可保留）</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>讀者不需要知道 — 刪除比解釋更尊重讀者</title><link>https://tarrragon.github.io/blog/report/reader-does-not-need-to-know/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/reader-does-not-need-to-know/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>文章中的 meta 資訊（「這篇文章的目的是…」「本文邊界是…」「先交代脈絡，否則…」）服務的是作者，不是讀者。讀者打開文章的問題是「這篇文章對我有什麼用」，而非「作者為什麼寫這篇文章」。&lt;/p>
&lt;h2 id="案例">案例&lt;/h2>
&lt;h3 id="案例一整理目的-blockquote">案例一：整理目的 blockquote&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>版本&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>修正前&lt;/td>
 &lt;td>&lt;code>&amp;gt; **整理目的**：這不是一次性事件的檢討報告，而是對「工具設計者的責任邊界」的反思。&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修正後&lt;/td>
 &lt;td>（整段刪除，改為直述讀者能帶走什麼）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「整理目的」告訴讀者作者為什麼寫這篇文章。但讀者的問題不是「你為什麼寫」，而是「我為什麼要讀」。兩者的差別：&lt;/p>
&lt;ul>
&lt;li>作者視角：「這是一篇反思，不是事件報告」→ 在做分類&lt;/li>
&lt;li>讀者視角：「讀完這篇我能改善什麼？」→ 在評估投入回報&lt;/li>
&lt;/ul>
&lt;h3 id="案例二脈絡解釋">案例二：脈絡解釋&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>版本&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>修正前&lt;/td>
 &lt;td>「先交代脈絡，否則後面的事件沒有參照點。」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修正後&lt;/td>
 &lt;td>（整句刪除，直接進入脈絡敘述）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這句話是寫給自己的編輯註記——「提醒自己為什麼要寫背景段」。讀者不需要知道作者的編輯考量。章節標題（「背景：我們怎麼管理版本和工作項目」）已經告訴讀者接下來是脈絡交代。&lt;/p>
&lt;h2 id="判斷標準">判斷標準&lt;/h2>
&lt;p>寫完一段 meta 描述後問：&lt;strong>這段話消失後，讀者的閱讀體驗會變差嗎？&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>「整理目的」消失 → 讀者仍然能從內文判斷這是反思而非報告 → 不會變差 → 刪除&lt;/li>
&lt;li>「先交代脈絡」消失 → 讀者仍然會讀到脈絡段 → 不會變差 → 刪除&lt;/li>
&lt;li>semver 背景段消失 → 讀者會在事件段卡住 → 會變差 → 保留&lt;/li>
&lt;/ul>
&lt;p>meta 資訊的本質是「替讀者做他已經能自己做的事」。讀者能從標題和內文推斷文章類型，不需要作者顯式宣告。&lt;/p>
&lt;h2 id="ai-寫作的傾向">AI 寫作的傾向&lt;/h2>
&lt;p>AI 生成的文章高頻出現 meta 資訊，因為 AI 的生成過程包含「規劃→組織→寫作」三步，meta 資訊是規劃步驟的殘留——AI 把內部的推理過程（「我接下來要先交代脈絡」）外露到了文章中。&lt;/p>
&lt;p>生成端防護：完成初稿後掃描所有 blockquote 和段首句，問「這句在描述內容還是在描述寫作過程？」描述寫作過程的句子刪除。&lt;/p>
&lt;p>&lt;strong>場景邊界&lt;/strong>：此判斷適用於短篇分享文。長篇技術文件或 RFC 的 scope 聲明（「本文不討論 X」）有不同作用——幫讀者快速判斷是否繼續讀，屬合理 meta。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>文章中的 meta 資訊（「這篇文章的目的是…」「本文邊界是…」「先交代脈絡，否則…」）服務的是作者，不是讀者。讀者打開文章的問題是「這篇文章對我有什麼用」，而非「作者為什麼寫這篇文章」。</p>
<h2 id="案例">案例</h2>
<h3 id="案例一整理目的-blockquote">案例一：整理目的 blockquote</h3>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修正前</td>
          <td><code>&gt; **整理目的**：這不是一次性事件的檢討報告，而是對「工具設計者的責任邊界」的反思。</code></td>
      </tr>
      <tr>
          <td>修正後</td>
          <td>（整段刪除，改為直述讀者能帶走什麼）</td>
      </tr>
  </tbody>
</table>
<p>「整理目的」告訴讀者作者為什麼寫這篇文章。但讀者的問題不是「你為什麼寫」，而是「我為什麼要讀」。兩者的差別：</p>
<ul>
<li>作者視角：「這是一篇反思，不是事件報告」→ 在做分類</li>
<li>讀者視角：「讀完這篇我能改善什麼？」→ 在評估投入回報</li>
</ul>
<h3 id="案例二脈絡解釋">案例二：脈絡解釋</h3>
<table>
  <thead>
      <tr>
          <th>版本</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>修正前</td>
          <td>「先交代脈絡，否則後面的事件沒有參照點。」</td>
      </tr>
      <tr>
          <td>修正後</td>
          <td>（整句刪除，直接進入脈絡敘述）</td>
      </tr>
  </tbody>
</table>
<p>這句話是寫給自己的編輯註記——「提醒自己為什麼要寫背景段」。讀者不需要知道作者的編輯考量。章節標題（「背景：我們怎麼管理版本和工作項目」）已經告訴讀者接下來是脈絡交代。</p>
<h2 id="判斷標準">判斷標準</h2>
<p>寫完一段 meta 描述後問：<strong>這段話消失後，讀者的閱讀體驗會變差嗎？</strong></p>
<ul>
<li>「整理目的」消失 → 讀者仍然能從內文判斷這是反思而非報告 → 不會變差 → 刪除</li>
<li>「先交代脈絡」消失 → 讀者仍然會讀到脈絡段 → 不會變差 → 刪除</li>
<li>semver 背景段消失 → 讀者會在事件段卡住 → 會變差 → 保留</li>
</ul>
<p>meta 資訊的本質是「替讀者做他已經能自己做的事」。讀者能從標題和內文推斷文章類型，不需要作者顯式宣告。</p>
<h2 id="ai-寫作的傾向">AI 寫作的傾向</h2>
<p>AI 生成的文章高頻出現 meta 資訊，因為 AI 的生成過程包含「規劃→組織→寫作」三步，meta 資訊是規劃步驟的殘留——AI 把內部的推理過程（「我接下來要先交代脈絡」）外露到了文章中。</p>
<p>生成端防護：完成初稿後掃描所有 blockquote 和段首句，問「這句在描述內容還是在描述寫作過程？」描述寫作過程的句子刪除。</p>
<p><strong>場景邊界</strong>：此判斷適用於短篇分享文。長篇技術文件或 RFC 的 scope 聲明（「本文不討論 X」）有不同作用——幫讀者快速判斷是否繼續讀，屬合理 meta。</p>
]]></content:encoded></item></channel></rss>