<?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>Strategy on Tarragon</title><link>https://tarrragon.github.io/blog/tags/strategy/</link><description>Recent content in Strategy on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/strategy/index.xml" rel="self" type="application/rss+xml"/><item><title>三層定義與職責表</title><link>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/three-layer-definition/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/three-layer-definition/</guid><description>&lt;p>測試分層的目的是讓每一層只負責一類問題，使得「哪種 bug 該被哪層抓到」有明確歸屬。三層之間存在語意斷層，單靠一層無論寫多少 test 都無法跨越另一層的職責。&lt;/p>
&lt;h2 id="三層的職責邊界">三層的職責邊界&lt;/h2>
&lt;h3 id="unit-test驗證程式碼邏輯">Unit Test：驗證程式碼邏輯&lt;/h3>
&lt;p>Unit test 驗證的對象是「開發者寫的程式碼是否按預期運作」。它的輸入和輸出都在程式碼控制範圍內 — 函式的參數、回傳值、狀態變化、例外拋出。&lt;/p>
&lt;p>Unit test 的盲區是所有程式碼以外的東西。外部服務的協議行為、網路傳輸的編碼方式、作業系統的檔案鎖定機制 — 這些不在 unit test 的驗證範圍內，因為 unit test 用 mock 取代了這些外部依賴。Mock 忠實模擬的是程式語言層面的 API 契約（方法簽名、參數型別、回傳值），不是外部服務的協議行為。&lt;/p>
&lt;p>app_tunnel 的 192 個 unit test 全部通過，但實機連線後鍵盤輸入無回應。原因是 WebSocket 的 text frame 與 binary frame 差異屬於協議層語意 — &lt;code>FakeWebSocketChannel&lt;/code> 的 &lt;code>sink.add(dynamic)&lt;/code> 接受任何型別，不區分 frame type（&lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1&lt;/a>）。192 個 test 驗證的是「Dart 程式碼邏輯正確」，沒有任何一個 test 的職責是驗證「ttyd 收到的 frame type 是否正確」。&lt;/p>
&lt;h3 id="protocol-integration-test驗證真實協議互動">Protocol Integration Test：驗證真實協議互動&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test&lt;/a> 驗證的對象是「程式碼和真實外部服務之間的協議互動是否正確」。它不用 mock，而是對真實的服務實例發送請求，觀察真實的回應。&lt;/p>
&lt;p>這一層的驗證目標包括：連線握手是否完成、認證流程是否正確、資料編碼是否符合對方期望、逾時行為是否合理。這些問題的答案不在程式碼裡，而是在程式碼與外部服務的互動過程中。&lt;/p>
&lt;p>app_tunnel 的 auth handshake 缺失就是典型案例。ttyd 要求連線後發送 auth token JSON frame，但 &lt;code>ConnectionManager&lt;/code> 沒有實作這個步驟 — &lt;code>FakeWebSocketChannel.ready&lt;/code> 立即完成不需認證，所有 test 看到的都是連線成功（&lt;a href="https://tarrragon.github.io/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2&lt;/a>）。對真實 ttyd 執行一個「連線後不發 auth token，斷言 timeout」的 test，就能暴露這個缺失。&lt;/p>
&lt;h3 id="screen-state-test驗證畫面狀態完整性">Screen State Test：驗證畫面狀態完整性&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/screen-state-test/" data-link-title="Screen State Test" data-link-desc="驗證使用者可見的畫面狀態覆蓋度和狀態間轉換完整性的 test 層級">Screen state test&lt;/a> 驗證的對象是「使用者可見的畫面狀態是否覆蓋所有情境」。它的關注點是畫面層級的狀態機 — loading、connected、error、reconnecting 等狀態之間的轉換是否完整，每個狀態下使用者看到什麼、能操作什麼。&lt;/p>
&lt;p>Screen state test 和 unit test 的區別在於斷言對象：unit test 斷言「函式回傳值是否正確」，screen state test 斷言「使用者看到的畫面是否正確」。同一段程式碼邏輯可能 unit test 通過（回傳值正確）但 screen state test 失敗（畫面沒顯示對應狀態），因為 UI 層的 binding 有問題。&lt;/p>
&lt;h2 id="三層對照">三層對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Unit Test&lt;/th>
 &lt;th>Protocol Integration Test&lt;/th>
 &lt;th>Screen State Test&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>全部 mock&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>UI 層 binding、畫面狀態完整性&lt;/td>
 &lt;td>內部邏輯錯誤、效能問題&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="數量與覆蓋率的關係">數量與覆蓋率的關係&lt;/h2>
&lt;p>測試數量和測試覆蓋率是兩個獨立的維度。192 個 unit test 提供的是 unit test 層的覆蓋率 — 程式碼邏輯的分支覆蓋。把 unit test 從 192 個加到 500 個，增加的仍然是同一層的覆蓋率，不會跨越到協議層或畫面層。&lt;/p></description><content:encoded><![CDATA[<p>測試分層的目的是讓每一層只負責一類問題，使得「哪種 bug 該被哪層抓到」有明確歸屬。三層之間存在語意斷層，單靠一層無論寫多少 test 都無法跨越另一層的職責。</p>
<h2 id="三層的職責邊界">三層的職責邊界</h2>
<h3 id="unit-test驗證程式碼邏輯">Unit Test：驗證程式碼邏輯</h3>
<p>Unit test 驗證的對象是「開發者寫的程式碼是否按預期運作」。它的輸入和輸出都在程式碼控制範圍內 — 函式的參數、回傳值、狀態變化、例外拋出。</p>
<p>Unit test 的盲區是所有程式碼以外的東西。外部服務的協議行為、網路傳輸的編碼方式、作業系統的檔案鎖定機制 — 這些不在 unit test 的驗證範圍內，因為 unit test 用 mock 取代了這些外部依賴。Mock 忠實模擬的是程式語言層面的 API 契約（方法簽名、參數型別、回傳值），不是外部服務的協議行為。</p>
<p>app_tunnel 的 192 個 unit test 全部通過，但實機連線後鍵盤輸入無回應。原因是 WebSocket 的 text frame 與 binary frame 差異屬於協議層語意 — <code>FakeWebSocketChannel</code> 的 <code>sink.add(dynamic)</code> 接受任何型別，不區分 frame type（<a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1</a>）。192 個 test 驗證的是「Dart 程式碼邏輯正確」，沒有任何一個 test 的職責是驗證「ttyd 收到的 frame type 是否正確」。</p>
<h3 id="protocol-integration-test驗證真實協議互動">Protocol Integration Test：驗證真實協議互動</h3>
<p><a href="/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test</a> 驗證的對象是「程式碼和真實外部服務之間的協議互動是否正確」。它不用 mock，而是對真實的服務實例發送請求，觀察真實的回應。</p>
<p>這一層的驗證目標包括：連線握手是否完成、認證流程是否正確、資料編碼是否符合對方期望、逾時行為是否合理。這些問題的答案不在程式碼裡，而是在程式碼與外部服務的互動過程中。</p>
<p>app_tunnel 的 auth handshake 缺失就是典型案例。ttyd 要求連線後發送 auth token JSON frame，但 <code>ConnectionManager</code> 沒有實作這個步驟 — <code>FakeWebSocketChannel.ready</code> 立即完成不需認證，所有 test 看到的都是連線成功（<a href="/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2</a>）。對真實 ttyd 執行一個「連線後不發 auth token，斷言 timeout」的 test，就能暴露這個缺失。</p>
<h3 id="screen-state-test驗證畫面狀態完整性">Screen State Test：驗證畫面狀態完整性</h3>
<p><a href="/blog/testing/knowledge-cards/screen-state-test/" data-link-title="Screen State Test" data-link-desc="驗證使用者可見的畫面狀態覆蓋度和狀態間轉換完整性的 test 層級">Screen state test</a> 驗證的對象是「使用者可見的畫面狀態是否覆蓋所有情境」。它的關注點是畫面層級的狀態機 — loading、connected、error、reconnecting 等狀態之間的轉換是否完整，每個狀態下使用者看到什麼、能操作什麼。</p>
<p>Screen state test 和 unit test 的區別在於斷言對象：unit test 斷言「函式回傳值是否正確」，screen state test 斷言「使用者看到的畫面是否正確」。同一段程式碼邏輯可能 unit test 通過（回傳值正確）但 screen state test 失敗（畫面沒顯示對應狀態），因為 UI 層的 binding 有問題。</p>
<h2 id="三層對照">三層對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Unit Test</th>
          <th>Protocol Integration Test</th>
          <th>Screen State Test</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>驗證對象</td>
          <td>程式碼邏輯</td>
          <td>程式碼與真實服務的協議互動</td>
          <td>使用者可見的畫面狀態</td>
      </tr>
      <tr>
          <td>外部依賴</td>
          <td>全部 mock</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>UI 層 binding、畫面狀態完整性</td>
          <td>內部邏輯錯誤、效能問題</td>
      </tr>
  </tbody>
</table>
<h2 id="數量與覆蓋率的關係">數量與覆蓋率的關係</h2>
<p>測試數量和測試覆蓋率是兩個獨立的維度。192 個 unit test 提供的是 unit test 層的覆蓋率 — 程式碼邏輯的分支覆蓋。把 unit test 從 192 個加到 500 個，增加的仍然是同一層的覆蓋率，不會跨越到協議層或畫面層。</p>
<p>層級缺失的問題無法用數量解決。如果整個 test suite 只有 unit test，即使覆蓋率 100%，protocol integration test 層和 screen state test 層的覆蓋率仍然是 0%。app_tunnel 的經驗是：在 unit test 層加更多 test 不會讓 frame type 問題浮現，因為 <code>FakeWebSocketChannel</code> 的行為在每一個 test 中都是一致的 — 一致地遮蔽了協議層差異。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Mock 如何在 API 層和協議層之間製造盲區 → <a href="/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">Mock 遮蔽機制分析</a></li>
<li>如何辨認「名義 integration test」 → <a href="/blog/testing/01-test-strategy-layers/nominal-integration-test/" data-link-title="「名義 integration test」的識別與修正" data-link-desc="test 名稱含 integration 但核心依賴全用 fake — 如何辨認、為什麼有害、怎麼修正命名和測試策略">名義 integration test 的識別與修正</a></li>
<li>判斷自己的服務是否需要 protocol integration test → <a href="/blog/testing/01-test-strategy-layers/when-protocol-integration-test/" data-link-title="判斷原則：什麼時候需要 protocol integration test" data-link-desc="從服務架構特徵判斷是否需要 protocol integration test 的決策流程 — 協議複雜度、mock 寬鬆度、失敗靜默度三個維度">判斷原則：什麼時候需要 protocol integration test</a></li>
<li>三層測試如何對應畫面狀態矩陣 → <a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一：畫面狀態機</a></li>
</ul>
]]></content:encoded></item><item><title>模組一：測試策略分層</title><link>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/</guid><description>&lt;p>回答「什麼測試抓什麼問題」。三層測試各自有明確的職責和盲區。192 個 mock test 全過但實機全壞的根因在層級缺失，不在數量不足。&lt;/p>
&lt;h2 id="對應-findings">對應 findings&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>TF-1&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1&lt;/a>&lt;/td>
 &lt;td>mock 模擬 API 層不模擬協議層 — &lt;strong>本模組主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TF-2&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2&lt;/a>&lt;/td>
 &lt;td>mock happy path 比真實服務寬鬆 → 功能缺失不可見&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TF-3&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2&lt;/a>&lt;/td>
 &lt;td>「名義 integration」全用 fake → 驗證內部狀態機非真實互動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 三層定義與職責表（從 _index.md 的表格擴展為完整論述）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Mock 遮蔽機制分析（API 層 vs 協議層 vs 環境層的斷裂點）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 「名義 integration test」的識別與修正&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 判斷原則：什麼時候需要 protocol integration test（決策表）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 反模式：用 mock 數量彌補 mock 盲區&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">monitoring 模組三 SDK 設計&lt;/a>：SDK 的自動攔截機制影響哪些錯誤能被 test 覆蓋&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一 畫面狀態機&lt;/a>：狀態矩陣直接轉成 screen state test case&lt;/li>
&lt;li>← &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/" data-link-title="模組二：Gate 與 Fallback 設計" data-link-desc="Biometric / Network / Auth / Permission — 每個 gate 成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">ux-design 模組二 Gate Fallback&lt;/a>：開發環境遮蔽 gate 問題的機制和 mock 遮蔽結構相同&lt;/li>
&lt;li>← work-log 案例入口：&lt;a href="https://tarrragon.github.io/blog/work-log/192-%E5%80%8B%E6%B8%AC%E8%A9%A6%E5%85%A8%E9%81%8E%E5%AF%A6%E6%A9%9F%E5%85%A8%E5%A3%9Emock-%E9%81%AE%E8%94%BD%E7%9C%9F%E5%AF%A6%E8%A1%8C%E7%82%BA%E7%9A%84%E4%B8%89%E5%B1%A4%E6%B8%AC%E8%A9%A6%E7%AD%96%E7%95%A5/" data-link-title="192 個測試全過、實機全壞：Mock 遮蔽真實行為的三層測試策略" data-link-desc="unit test 全綠、實機部署後功能整片壞掉。mock-only 策略的結構盲區（text vs binary frame、缺 auth handshake、ANSI 多樣性被 FakeWebSocketChannel 遮蔽），以及分層測試各抓什麼、各遮蔽什麼。">192 個測試全過、實機全壞&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「什麼測試抓什麼問題」。三層測試各自有明確的職責和盲區。192 個 mock test 全過但實機全壞的根因在層級缺失，不在數量不足。</p>
<h2 id="對應-findings">對應 findings</h2>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>來源</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TF-1</td>
          <td><a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1</a></td>
          <td>mock 模擬 API 層不模擬協議層 — <strong>本模組主寫</strong></td>
      </tr>
      <tr>
          <td>TF-2</td>
          <td><a href="/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2</a></td>
          <td>mock happy path 比真實服務寬鬆 → 功能缺失不可見</td>
      </tr>
      <tr>
          <td>TF-3</td>
          <td><a href="/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2</a></td>
          <td>「名義 integration」全用 fake → 驗證內部狀態機非真實互動</td>
      </tr>
  </tbody>
</table>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> 三層定義與職責表（從 _index.md 的表格擴展為完整論述）</li>
<li><input checked="" disabled="" type="checkbox"> Mock 遮蔽機制分析（API 層 vs 協議層 vs 環境層的斷裂點）</li>
<li><input checked="" disabled="" type="checkbox"> 「名義 integration test」的識別與修正</li>
<li><input checked="" disabled="" type="checkbox"> 判斷原則：什麼時候需要 protocol integration test（決策表）</li>
<li><input checked="" disabled="" type="checkbox"> 反模式：用 mock 數量彌補 mock 盲區</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">monitoring 模組三 SDK 設計</a>：SDK 的自動攔截機制影響哪些錯誤能被 test 覆蓋</li>
<li>→ <a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一 畫面狀態機</a>：狀態矩陣直接轉成 screen state test case</li>
<li>← <a href="/blog/ux-design/02-gate-fallback/" data-link-title="模組二：Gate 與 Fallback 設計" data-link-desc="Biometric / Network / Auth / Permission — 每個 gate 成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">ux-design 模組二 Gate Fallback</a>：開發環境遮蔽 gate 問題的機制和 mock 遮蔽結構相同</li>
<li>← work-log 案例入口：<a href="/blog/work-log/192-%E5%80%8B%E6%B8%AC%E8%A9%A6%E5%85%A8%E9%81%8E%E5%AF%A6%E6%A9%9F%E5%85%A8%E5%A3%9Emock-%E9%81%AE%E8%94%BD%E7%9C%9F%E5%AF%A6%E8%A1%8C%E7%82%BA%E7%9A%84%E4%B8%89%E5%B1%A4%E6%B8%AC%E8%A9%A6%E7%AD%96%E7%95%A5/" data-link-title="192 個測試全過、實機全壞：Mock 遮蔽真實行為的三層測試策略" data-link-desc="unit test 全綠、實機部署後功能整片壞掉。mock-only 策略的結構盲區（text vs binary frame、缺 auth handshake、ANSI 多樣性被 FakeWebSocketChannel 遮蔽），以及分層測試各抓什麼、各遮蔽什麼。">192 個測試全過、實機全壞</a></li>
</ul>
]]></content:encoded></item><item><title>判斷原則：什麼時候需要 protocol integration test</title><link>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/when-protocol-integration-test/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/when-protocol-integration-test/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test&lt;/a> 有成本 — 需要真實服務實例、環境準備、執行速度較慢、結果可能因環境差異而不穩定。判斷是否需要這一層測試，依據的是服務架構的特徵，而非主觀的「寫多一點比較安心」。&lt;/p>
&lt;h2 id="三個判斷維度">三個判斷維度&lt;/h2>
&lt;h3 id="維度一協議複雜度">維度一：協議複雜度&lt;/h3>
&lt;p>程式碼和外部服務之間的協議是否存在 API 層無法描述的語意？&lt;/p>
&lt;p>HTTP REST API 的協議複雜度相對低：request body 是 JSON、response body 是 JSON、status code 有明確語意。Mock 一個 REST endpoint（回傳固定 JSON）和真實 endpoint 的行為差異主要在效能和邊界案例，核心語意差距小。&lt;/p>
&lt;p>WebSocket 協議的複雜度較高：連線握手、frame type（text / binary / ping / pong / close）、分片（fragmentation）、壓縮擴展（permessage-deflate）、子協議協商 — 這些語意在 API 層（&lt;code>sink.add(dynamic)&lt;/code>）是不可見的。gRPC 的 streaming、deadline propagation、metadata header 也有類似特徵。&lt;/p>
&lt;p>判斷問題：&lt;strong>API 簽名是否隱藏了協議層的行為分支？&lt;/strong> 如果 API 用 &lt;code>dynamic&lt;/code>、&lt;code>Object&lt;/code>、&lt;code>Any&lt;/code> 等寬泛型別接受輸入，而協議層對不同輸入有不同處理方式，這就是需要 protocol integration test 的訊號。&lt;/p>
&lt;p>app_tunnel 的 &lt;code>sink.add(dynamic)&lt;/code> 就是這個模式 — API 簽名不區分 &lt;code>String&lt;/code> 和 &lt;code>Uint8List&lt;/code>，但協議層對兩者產生不同的 frame type（&lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1&lt;/a>）。&lt;/p>
&lt;h3 id="維度二mock-寬鬆度">維度二：Mock 寬鬆度&lt;/h3>
&lt;p>Mock 的行為是否比真實服務更寬容？&lt;/p>
&lt;p>Mock 通常是「最小可用」的實作 — 能讓 test 通過就好。這意味著 mock 的行為往往比真實服務寬鬆：不檢查認證、不限制速率、不要求特定順序、不區分輸入格式。&lt;/p>
&lt;p>寬鬆本身不是問題，但寬鬆程度和真實服務的差距決定了 mock 遮蔽的風險大小。判斷問題：&lt;strong>Mock 跳過了真實服務的哪些步驟？每個被跳過的步驟在業務上是否關鍵？&lt;/strong>&lt;/p>
&lt;p>app_tunnel 的 &lt;code>FakeWebSocketChannel&lt;/code> 跳過了 auth handshake — &lt;code>ready&lt;/code> 立即完成不需認證。Auth handshake 在業務上是關鍵步驟（沒有認證，ttyd 不推送資料），mock 跳過這一步讓「功能根本沒實作」變得不可見（&lt;a href="https://tarrragon.github.io/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2&lt;/a>）。&lt;/p>
&lt;p>逐項列出 mock 跳過的步驟是一個實用的 audit 方法。寫出「&lt;code>FakeWebSocketChannel&lt;/code> 和 &lt;code>IOWebSocketChannel&lt;/code> 的行為差異清單」，每一個差異點就是潛在的遮蔽風險。&lt;/p>
&lt;h3 id="維度三失敗靜默度">維度三：失敗靜默度&lt;/h3>
&lt;p>外部服務收到非預期輸入時，回應是明確的錯誤還是靜默忽略？&lt;/p>
&lt;p>如果外部服務對錯誤輸入回傳 HTTP 400 或斷線，問題在實機測試時會快速浮現 — 程式碼進入 error 狀態，開發者看到明確的錯誤訊息。但如果外部服務靜默忽略，問題表現為「連線成功但沒有回應」，debug 方向可能完全錯誤。&lt;/p>
&lt;p>ttyd 收到 binary frame 時靜默忽略，不回傳錯誤碼也不斷線。這讓問題的表現從「frame type 錯誤」變成「終端機無回應」，開發者的 debug 方向是「為什麼 terminal 沒反應」而非「為什麼 frame type 不對」。&lt;/p>
&lt;p>判斷問題：&lt;strong>外部服務是否有靜默忽略的行為？&lt;/strong> 如果有，protocol integration test 的價值更高 — 因為即使在實機測試階段，靜默忽略也會增加 debug 成本。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test</a> 有成本 — 需要真實服務實例、環境準備、執行速度較慢、結果可能因環境差異而不穩定。判斷是否需要這一層測試，依據的是服務架構的特徵，而非主觀的「寫多一點比較安心」。</p>
<h2 id="三個判斷維度">三個判斷維度</h2>
<h3 id="維度一協議複雜度">維度一：協議複雜度</h3>
<p>程式碼和外部服務之間的協議是否存在 API 層無法描述的語意？</p>
<p>HTTP REST API 的協議複雜度相對低：request body 是 JSON、response body 是 JSON、status code 有明確語意。Mock 一個 REST endpoint（回傳固定 JSON）和真實 endpoint 的行為差異主要在效能和邊界案例，核心語意差距小。</p>
<p>WebSocket 協議的複雜度較高：連線握手、frame type（text / binary / ping / pong / close）、分片（fragmentation）、壓縮擴展（permessage-deflate）、子協議協商 — 這些語意在 API 層（<code>sink.add(dynamic)</code>）是不可見的。gRPC 的 streaming、deadline propagation、metadata header 也有類似特徵。</p>
<p>判斷問題：<strong>API 簽名是否隱藏了協議層的行為分支？</strong> 如果 API 用 <code>dynamic</code>、<code>Object</code>、<code>Any</code> 等寬泛型別接受輸入，而協議層對不同輸入有不同處理方式，這就是需要 protocol integration test 的訊號。</p>
<p>app_tunnel 的 <code>sink.add(dynamic)</code> 就是這個模式 — API 簽名不區分 <code>String</code> 和 <code>Uint8List</code>，但協議層對兩者產生不同的 frame type（<a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1</a>）。</p>
<h3 id="維度二mock-寬鬆度">維度二：Mock 寬鬆度</h3>
<p>Mock 的行為是否比真實服務更寬容？</p>
<p>Mock 通常是「最小可用」的實作 — 能讓 test 通過就好。這意味著 mock 的行為往往比真實服務寬鬆：不檢查認證、不限制速率、不要求特定順序、不區分輸入格式。</p>
<p>寬鬆本身不是問題，但寬鬆程度和真實服務的差距決定了 mock 遮蔽的風險大小。判斷問題：<strong>Mock 跳過了真實服務的哪些步驟？每個被跳過的步驟在業務上是否關鍵？</strong></p>
<p>app_tunnel 的 <code>FakeWebSocketChannel</code> 跳過了 auth handshake — <code>ready</code> 立即完成不需認證。Auth handshake 在業務上是關鍵步驟（沒有認證，ttyd 不推送資料），mock 跳過這一步讓「功能根本沒實作」變得不可見（<a href="/blog/testing/cases/auth-handshake-missing-mock-blindspot/" data-link-title="T.C2 Auth handshake 邏輯缺失被 FakeWebSocketChannel 遮蔽" data-link-desc="ttyd 連線後需要發送 auth token JSON frame 完成認證，整個邏輯未實作 — FakeWebSocketChannel 的 ready 立即完成不需認證，test 永遠看到連線成功">T.C2</a>）。</p>
<p>逐項列出 mock 跳過的步驟是一個實用的 audit 方法。寫出「<code>FakeWebSocketChannel</code> 和 <code>IOWebSocketChannel</code> 的行為差異清單」，每一個差異點就是潛在的遮蔽風險。</p>
<h3 id="維度三失敗靜默度">維度三：失敗靜默度</h3>
<p>外部服務收到非預期輸入時，回應是明確的錯誤還是靜默忽略？</p>
<p>如果外部服務對錯誤輸入回傳 HTTP 400 或斷線，問題在實機測試時會快速浮現 — 程式碼進入 error 狀態，開發者看到明確的錯誤訊息。但如果外部服務靜默忽略，問題表現為「連線成功但沒有回應」，debug 方向可能完全錯誤。</p>
<p>ttyd 收到 binary frame 時靜默忽略，不回傳錯誤碼也不斷線。這讓問題的表現從「frame type 錯誤」變成「終端機無回應」，開發者的 debug 方向是「為什麼 terminal 沒反應」而非「為什麼 frame type 不對」。</p>
<p>判斷問題：<strong>外部服務是否有靜默忽略的行為？</strong> 如果有，protocol integration test 的價值更高 — 因為即使在實機測試階段，靜默忽略也會增加 debug 成本。</p>
<h2 id="決策流程">決策流程</h2>
<p>以下流程不追求完備覆蓋所有情境，而是提供一個起點，根據上述三個維度的組合判斷 protocol integration test 的必要性。</p>
<p><strong>協議複雜度高（API 層和協議層有語意斷裂）：</strong> 需要 protocol integration test。即使 mock 寬鬆度低、失敗回報明確，語意斷裂本身就是 mock 結構性無法覆蓋的盲區。</p>
<p><strong>協議複雜度低，但 mock 寬鬆度高（mock 跳過業務關鍵步驟）：</strong> 需要 protocol integration test。Mock 跳過的步驟越多，「功能缺失不可見」的風險越大。</p>
<p><strong>協議複雜度低，mock 寬鬆度低：</strong> 依失敗靜默度判斷。如果外部服務靜默忽略錯誤，protocol integration test 有較高價值；如果錯誤回報明確，可以依賴實機測試階段的 error 來發現問題。</p>
<p><strong>成本極低的情境：</strong> 當外部服務可以在 test 環境輕鬆啟動時（自用工具 server+client 同機、Docker 一行啟動的 open source service），protocol integration test 的成本門檻大幅降低，三個維度中任何一個有疑慮就值得寫。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想實作 protocol integration test → <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">模組三：協議整合測試</a></li>
<li>理解 mock 遮蔽的結構性原因 → <a href="/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">Mock 遮蔽機制分析</a></li>
<li>反模式：試圖用更多 mock test 補救 → <a href="/blog/testing/01-test-strategy-layers/anti-pattern-mock-quantity/" data-link-title="反模式：用 mock 數量彌補 mock 盲區" data-link-desc="為什麼增加 mock test 數量無法跨越 mock 的結構性盲區 — 從 192 個 test 全過的案例拆解數量與覆蓋率的真正關係">反模式：用 mock 數量彌補 mock 盲區</a></li>
</ul>
]]></content:encoded></item><item><title>反模式：用 mock 數量彌補 mock 盲區</title><link>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/anti-pattern-mock-quantity/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/01-test-strategy-layers/anti-pattern-mock-quantity/</guid><description>&lt;p>當 mock test 全過但實機出問題時，常見的第一反應是「test 不夠多」或「覆蓋率不夠高」。這個反應假設 mock test 的問題在數量，而實際上問題在層級 — mock test 驗證的對象和實機暴露的問題不在同一層。增加 mock test 數量擴展的是同一層的覆蓋範圍，不會跨越到另一層。&lt;/p>
&lt;h2 id="數量與層級的區別">數量與層級的區別&lt;/h2>
&lt;p>app_tunnel 的 192 個 unit test 覆蓋了 &lt;code>ConnectionManager&lt;/code>、&lt;code>AnsiParser&lt;/code>、&lt;code>TerminalBuffer&lt;/code> 等元件的邏輯分支。如果在 mock test 全過但實機失敗後，反應是「再寫 50 個 test」，新寫的 test 會使用同一個 &lt;code>FakeWebSocketChannel&lt;/code>，測試更多的邏輯分支 — 更多的輸入組合、更多的邊界條件、更多的錯誤處理路徑。&lt;/p>
&lt;p>這 50 個新 test 和原來的 192 個 test 在同一個 mock 環境中執行，受到同一個 &lt;code>FakeWebSocketChannel&lt;/code> 的行為限制。&lt;code>FakeWebSocketChannel&lt;/code> 不區分 text frame 和 binary frame — 這個限制在第 1 個 test 和第 242 個 test 中都一樣。數量增加了，遮蔽範圍沒有改變。&lt;/p>
&lt;p>用類比說明：用純水測試淨水器的過濾效果，不管測 1 杯還是 1000 杯，結論都是「水很乾淨」。問題在測試材料 — 需要用含有雜質的水測試才能驗證過濾功能。Mock 是純水，真實服務互動是含雜質的水。&lt;/p>
&lt;h2 id="覆蓋率指標的盲點">覆蓋率指標的盲點&lt;/h2>
&lt;p>Line coverage 和 branch coverage 衡量的是「程式碼中有多少行 / 分支被 test 執行過」。這些指標在同一層 test 內有意義 — 100% branch coverage 的 unit test 確保每個 if/else 都被走過。&lt;/p>
&lt;p>但覆蓋率指標不區分 test 的依賴環境。一個使用 &lt;code>FakeWebSocketChannel&lt;/code> 的 test 和一個使用 &lt;code>IOWebSocketChannel&lt;/code> 的 test 走過同一行 &lt;code>sink.add(data)&lt;/code> — 在覆蓋率報告中是同一行被覆蓋，但驗證的語意完全不同。&lt;/p>
&lt;p>覆蓋率 100% 意味著「在 mock 環境中，所有程式碼分支都被走過」。這不等於「在真實環境中，所有程式碼分支的行為都是正確的」。app_tunnel 的 &lt;code>sendData()&lt;/code> 在覆蓋率報告中是「已覆蓋」的，但覆蓋它的 test 用的是不區分 frame type 的 fake。&lt;/p>
&lt;h2 id="這個反模式如何在團隊中擴散">這個反模式如何在團隊中擴散&lt;/h2>
&lt;p>「test 不夠多」是一個容易執行、容易衡量的回應。在沒有獨立 QA 驗收流程的團隊中（覆蓋率報告是主要品質指標），寫更多 test 可以提高覆蓋率數字，覆蓋率數字上升給團隊信心。相比之下，「需要一個新的 test 層級」需要建置新的 test 環境、學習不同的 test 技術、接受較慢的執行速度。&lt;/p>
&lt;p>這個成本差異讓團隊傾向於在既有的 mock test 層加量，而非引入新的 test 層。每一輪加量後覆蓋率上升，團隊信心增加，但 mock 遮蔽的盲區從未被觸及。問題在下一次實機測試或 production incident 中再次浮現，觸發新一輪的「test 不夠多」反應。&lt;/p>
&lt;p>打破這個循環的起點是區分「同層覆蓋率不足」和「層級缺失」。如果問題是同層覆蓋率不足（某個分支沒被 test 走到），加 test 有效。如果問題是層級缺失（mock 結構性地遮蔽了某類行為），加同一層的 test 無效，需要引入新的 test 層級。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;p>以下訊號指向「層級缺失」而非「數量不足」：&lt;/p>
&lt;p>&lt;strong>test 全過但實機失敗的 bug 類型集中在外部互動&lt;/strong>：連線問題、認證問題、資料格式問題、逾時問題 — 這些問題的共同特徵是發生在程式碼與外部服務的邊界上，不是程式碼內部的邏輯錯誤。&lt;/p>
&lt;p>&lt;strong>修復後原有 test 不需要改動&lt;/strong>：如果 bug 修復只加了新程式碼（例如新增 auth handshake 步驟）而原有 test 全部不受影響，說明原有 test 從一開始就沒有覆蓋這個行為 — 整個 test 層級不涵蓋這類行為。&lt;/p>
&lt;p>&lt;strong>bug 修復是型別轉換或編碼調整&lt;/strong>：&lt;code>if (data is Uint8List) sink.add(String.fromCharCodes(data))&lt;/code> 這類修復改變的是資料在協議層的表現，不是程式邏輯。在 mock 環境中，這個修改前後的行為完全相同 — mock 不區分 frame type。&lt;/p>
&lt;p>區分「同層覆蓋率不足」和「層級缺失」之後，回到&lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/three-layer-definition/" data-link-title="三層定義與職責表" data-link-desc="Unit Test / Protocol Integration Test / Screen State Test 各層職責、驗證目標與盲區的完整論述">三層定義與職責表&lt;/a>確認每層的邊界。Mock 遮蔽的結構性原因在 &lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">Mock 遮蔽機制分析&lt;/a>中展開。如果判斷結果是層級缺失，&lt;a href="https://tarrragon.github.io/blog/testing/01-test-strategy-layers/when-protocol-integration-test/" data-link-title="判斷原則：什麼時候需要 protocol integration test" data-link-desc="從服務架構特徵判斷是否需要 protocol integration test 的決策流程 — 協議複雜度、mock 寬鬆度、失敗靜默度三個維度">判斷原則：什麼時候需要 protocol integration test&lt;/a> 提供引入新層級的決策流程。&lt;/p></description><content:encoded><![CDATA[<p>當 mock test 全過但實機出問題時，常見的第一反應是「test 不夠多」或「覆蓋率不夠高」。這個反應假設 mock test 的問題在數量，而實際上問題在層級 — mock test 驗證的對象和實機暴露的問題不在同一層。增加 mock test 數量擴展的是同一層的覆蓋範圍，不會跨越到另一層。</p>
<h2 id="數量與層級的區別">數量與層級的區別</h2>
<p>app_tunnel 的 192 個 unit test 覆蓋了 <code>ConnectionManager</code>、<code>AnsiParser</code>、<code>TerminalBuffer</code> 等元件的邏輯分支。如果在 mock test 全過但實機失敗後，反應是「再寫 50 個 test」，新寫的 test 會使用同一個 <code>FakeWebSocketChannel</code>，測試更多的邏輯分支 — 更多的輸入組合、更多的邊界條件、更多的錯誤處理路徑。</p>
<p>這 50 個新 test 和原來的 192 個 test 在同一個 mock 環境中執行，受到同一個 <code>FakeWebSocketChannel</code> 的行為限制。<code>FakeWebSocketChannel</code> 不區分 text frame 和 binary frame — 這個限制在第 1 個 test 和第 242 個 test 中都一樣。數量增加了，遮蔽範圍沒有改變。</p>
<p>用類比說明：用純水測試淨水器的過濾效果，不管測 1 杯還是 1000 杯，結論都是「水很乾淨」。問題在測試材料 — 需要用含有雜質的水測試才能驗證過濾功能。Mock 是純水，真實服務互動是含雜質的水。</p>
<h2 id="覆蓋率指標的盲點">覆蓋率指標的盲點</h2>
<p>Line coverage 和 branch coverage 衡量的是「程式碼中有多少行 / 分支被 test 執行過」。這些指標在同一層 test 內有意義 — 100% branch coverage 的 unit test 確保每個 if/else 都被走過。</p>
<p>但覆蓋率指標不區分 test 的依賴環境。一個使用 <code>FakeWebSocketChannel</code> 的 test 和一個使用 <code>IOWebSocketChannel</code> 的 test 走過同一行 <code>sink.add(data)</code> — 在覆蓋率報告中是同一行被覆蓋，但驗證的語意完全不同。</p>
<p>覆蓋率 100% 意味著「在 mock 環境中，所有程式碼分支都被走過」。這不等於「在真實環境中，所有程式碼分支的行為都是正確的」。app_tunnel 的 <code>sendData()</code> 在覆蓋率報告中是「已覆蓋」的，但覆蓋它的 test 用的是不區分 frame type 的 fake。</p>
<h2 id="這個反模式如何在團隊中擴散">這個反模式如何在團隊中擴散</h2>
<p>「test 不夠多」是一個容易執行、容易衡量的回應。在沒有獨立 QA 驗收流程的團隊中（覆蓋率報告是主要品質指標），寫更多 test 可以提高覆蓋率數字，覆蓋率數字上升給團隊信心。相比之下，「需要一個新的 test 層級」需要建置新的 test 環境、學習不同的 test 技術、接受較慢的執行速度。</p>
<p>這個成本差異讓團隊傾向於在既有的 mock test 層加量，而非引入新的 test 層。每一輪加量後覆蓋率上升，團隊信心增加，但 mock 遮蔽的盲區從未被觸及。問題在下一次實機測試或 production incident 中再次浮現，觸發新一輪的「test 不夠多」反應。</p>
<p>打破這個循環的起點是區分「同層覆蓋率不足」和「層級缺失」。如果問題是同層覆蓋率不足（某個分支沒被 test 走到），加 test 有效。如果問題是層級缺失（mock 結構性地遮蔽了某類行為），加同一層的 test 無效，需要引入新的 test 層級。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>以下訊號指向「層級缺失」而非「數量不足」：</p>
<p><strong>test 全過但實機失敗的 bug 類型集中在外部互動</strong>：連線問題、認證問題、資料格式問題、逾時問題 — 這些問題的共同特徵是發生在程式碼與外部服務的邊界上，不是程式碼內部的邏輯錯誤。</p>
<p><strong>修復後原有 test 不需要改動</strong>：如果 bug 修復只加了新程式碼（例如新增 auth handshake 步驟）而原有 test 全部不受影響，說明原有 test 從一開始就沒有覆蓋這個行為 — 整個 test 層級不涵蓋這類行為。</p>
<p><strong>bug 修復是型別轉換或編碼調整</strong>：<code>if (data is Uint8List) sink.add(String.fromCharCodes(data))</code> 這類修復改變的是資料在協議層的表現，不是程式邏輯。在 mock 環境中，這個修改前後的行為完全相同 — mock 不區分 frame type。</p>
<p>區分「同層覆蓋率不足」和「層級缺失」之後，回到<a href="/blog/testing/01-test-strategy-layers/three-layer-definition/" data-link-title="三層定義與職責表" data-link-desc="Unit Test / Protocol Integration Test / Screen State Test 各層職責、驗證目標與盲區的完整論述">三層定義與職責表</a>確認每層的邊界。Mock 遮蔽的結構性原因在 <a href="/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">Mock 遮蔽機制分析</a>中展開。如果判斷結果是層級缺失，<a href="/blog/testing/01-test-strategy-layers/when-protocol-integration-test/" data-link-title="判斷原則：什麼時候需要 protocol integration test" data-link-desc="從服務架構特徵判斷是否需要 protocol integration test 的決策流程 — 協議複雜度、mock 寬鬆度、失敗靜默度三個維度">判斷原則：什麼時候需要 protocol integration test</a> 提供引入新層級的決策流程。</p>
]]></content:encoded></item><item><title>商業概念與策略分析</title><link>https://tarrragon.github.io/blog/business/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/business/</guid><description>&lt;p>商業教材的核心目標是讓工程背景的讀者讀懂商業分析語言，建立判斷市場、新創、產業結構與職涯走向的框架。技術人讀 VC、創辦人、策略分析師寫的文章時常被一連串縮寫（COGS、CAC、LTV、FDE、PLG）擋在門外；本教材把這些術語拆成可獨立查閱的卡片，並補上分類體系、閱讀框架與案例拆解，讓讀者能把社群上的商業貼文系統化解構。&lt;/p>
&lt;p>本教材採四層結構。第一層是 atomic knowledge card，整理單一商業術語的核心概念、概念位置、可觀察訊號與判讀方式。第二層是分類索引，依商業模式、單位經濟、進入市場、競爭策略、市場動態、資本估值與執行知識把卡片分組。第三層是閱讀框架，幫助讀者判斷一篇商業分析的讀者定位、寫作目的與可信度。第四層是案例拆解，用 WRAP 框架拆解具體市場事件、抽出可遷移的判讀骨架。&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;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e5%95%86%e6%a5%ad%e6%a8%a1%e5%bc%8f" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">商業模式&lt;/a>&lt;/td>
 &lt;td>說明公司賣什麼、賣給誰、怎麼收費&lt;/td>
 &lt;td>SaaS、Vertical SaaS、Horizontal SaaS、CDP、Enterprise License&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e5%96%ae%e4%bd%8d%e7%b6%93%e6%bf%9f" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">單位經濟&lt;/a>&lt;/td>
 &lt;td>說明每個客戶或每筆交易的成本與利潤結構&lt;/td>
 &lt;td>COGS、Gross Margin、Marginal Cost、P&amp;amp;L、Burn Rate、Runway&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e9%80%b2%e5%85%a5%e5%b8%82%e5%a0%b4" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">進入市場&lt;/a>&lt;/td>
 &lt;td>說明用什麼通路與銷售模式把產品賣出去&lt;/td>
 &lt;td>GTM、PLG、FDE、JV、CAC&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e7%ab%b6%e7%88%ad%e8%ad%b7%e5%9f%8e%e6%b2%b3" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">競爭護城河&lt;/a>&lt;/td>
 &lt;td>說明為什麼客戶留下來、為什麼別人打不進來&lt;/td>
 &lt;td>Lock-in、Switching Cost、Retention、Thin Wrapper、Fat Data / Fat Skill、Connector&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e5%b8%82%e5%a0%b4%e5%8b%95%e6%85%8b" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">市場動態&lt;/a>&lt;/td>
 &lt;td>說明賽道處在什麼階段、競爭強度、需求類型&lt;/td>
 &lt;td>Red / Blue Ocean、Consolidation Cycle、Niche Market、High Stickiness、Rigid Demand、Frontier Capability、Distribution&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e8%b3%87%e6%9c%ac%e4%bc%b0%e5%80%bc" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">資本估值&lt;/a>&lt;/td>
 &lt;td>說明新創 / 公司價值怎麼被定價、被誰定價、何時崩塌&lt;/td>
 &lt;td>VC、PE、Valuation、Valuation Compression、Unit Economics、LTV&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e5%9f%b7%e8%a1%8c%e7%9f%a5%e8%ad%98" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">執行知識&lt;/a>&lt;/td>
 &lt;td>說明把產品做出來、把客戶服務好的隱性能力&lt;/td>
 &lt;td>Tacit Knowledge、Evaluation Set、PRD、Wireframe、Vibe Code、Judgment Stake、Junior Buffer&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/business/reading-frameworks/" data-link-title="閱讀商業分析的框架" data-link-desc="幫助讀者識別商業分析文章的讀者定位、寫作目的與可信度，避免誤讀策略分析、產業內幕與大眾財經">閱讀框架&lt;/a> 處理「眼前這篇文章是寫給誰看、目的是什麼、該怎麼讀」。看到一篇分析時先用閱讀框架定位文章類型，再用分類卡片解碼術語。&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解&lt;/a> 則是把整個流程實作出來—每篇文章拿一個具體市場事件（例如 Claude for Legal 推出、CoreWeave 收購 Bufstream），用 WRAP 結構走完一遍判讀。&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>工程背景、想讀懂商業分析的人&lt;/td>
 &lt;td>商業模式 → 單位經濟 → 進入市場&lt;/td>
 &lt;td>能看懂 SaaS、CAC、PLG 等基本縮寫構成的句子&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;a href="https://tarrragon.github.io/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解&lt;/a> 看完整 WRAP 範例&lt;/td>
 &lt;td>能識別文章類型、目標讀者、引用的概念與隱含的判斷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自己寫 WRAP 拆解&lt;/td>
 &lt;td>想練習結構化分析市場事件的人&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解 _index&lt;/a> → 三篇範例 → 套用結構模板&lt;/td>
 &lt;td>能用 WRAP 拆任何市場事件、產出可遷移的判讀框架&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="怎麼擴充這個模組">怎麼擴充這個模組&lt;/h2>
&lt;p>擴充走兩條路、依內容類型決定。&lt;/p>
&lt;h3 id="新術語擴充-knowledge-cards">新術語：擴充 knowledge-cards&lt;/h3>
&lt;p>新術語從社群貼文或書中出現時：&lt;/p>
&lt;ol>
&lt;li>用 &lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e5%bb%ba%e5%8d%a1%e5%88%a4%e6%ba%96" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">建卡判準&lt;/a> 判斷該術語是否值得獨立建卡。&lt;/li>
&lt;li>用 &lt;a href="#%e5%88%86%e9%a1%9e%e9%ab%94%e7%b3%bb">分類體系&lt;/a> 找到該卡片應歸屬的分類。&lt;/li>
&lt;li>用 &lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/#%e5%8d%a1%e7%89%87%e6%a0%bc%e5%bc%8f" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">卡片格式&lt;/a> 寫卡，遵循「核心概念、概念位置、可觀察訊號、判讀方式」四段結構。&lt;/li>
&lt;li>在 &lt;code>knowledge-cards/_index.md&lt;/code> 對應分類表格內加入連結。&lt;/li>
&lt;/ol>
&lt;p>不適合建卡的術語（過度寬泛、僅是字面翻譯、只能在原文中成立）應在分析文章中直接補清楚，避免建出單薄卡片。&lt;/p>
&lt;h3 id="新市場事件擴充-case-analyses">新市場事件：擴充 case-analyses&lt;/h3>
&lt;p>看到值得拆解的市場事件（M&amp;amp;A、產品推出、IPO、產業整併、政策變動）時：&lt;/p>
&lt;ol>
&lt;li>用 &lt;a href="https://tarrragon.github.io/blog/business/reading-frameworks/reader-purpose-matrix/" data-link-title="媒介—讀者—目的矩陣" data-link-desc="用媒介、讀者、目的三軸定位一篇商業分析的類型，避免把策略分析誤讀成投資建議或把產業內幕誤讀成大眾財經">媒介—讀者—目的矩陣&lt;/a> 先定位原文類型。&lt;/li>
&lt;li>用 &lt;a href="https://tarrragon.github.io/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解的 WRAP 結構模板&lt;/a> 逐段填寫。&lt;/li>
&lt;li>確保每個 Widen Option 都有對應 Reality Test、結尾必須給可遷移的判讀框架表。&lt;/li>
&lt;li>Tripwire 段必須具體可監控（不能寫「再觀察」這種模糊話）。&lt;/li>
&lt;/ol>
&lt;p>如果事件無法產出可遷移框架（只是孤立特例）、放筆記裡即可、不要硬寫成案例。&lt;/p></description><content:encoded><![CDATA[<p>商業教材的核心目標是讓工程背景的讀者讀懂商業分析語言，建立判斷市場、新創、產業結構與職涯走向的框架。技術人讀 VC、創辦人、策略分析師寫的文章時常被一連串縮寫（COGS、CAC、LTV、FDE、PLG）擋在門外；本教材把這些術語拆成可獨立查閱的卡片，並補上分類體系、閱讀框架與案例拆解，讓讀者能把社群上的商業貼文系統化解構。</p>
<p>本教材採四層結構。第一層是 atomic knowledge card，整理單一商業術語的核心概念、概念位置、可觀察訊號與判讀方式。第二層是分類索引，依商業模式、單位經濟、進入市場、競爭策略、市場動態、資本估值與執行知識把卡片分組。第三層是閱讀框架，幫助讀者判斷一篇商業分析的讀者定位、寫作目的與可信度。第四層是案例拆解，用 WRAP 框架拆解具體市場事件、抽出可遷移的判讀骨架。</p>
<h2 id="分類體系">分類體系</h2>
<p>商業概念分成七個主題分類加一層閱讀框架。每個分類負責一段商業推理責任：商業模式說明公司怎麼賺錢、單位經濟說明每個客戶帶來多少利潤、進入市場說明怎麼把客戶簽進來、競爭護城河說明為什麼客戶不會離開、市場動態說明賽道現在是什麼狀態、資本估值說明財務語言怎麼影響定價、執行知識說明把產品做出來的隱性能力。</p>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>承擔的商業推理責任</th>
          <th>典型術語</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/business/knowledge-cards/#%e5%95%86%e6%a5%ad%e6%a8%a1%e5%bc%8f" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">商業模式</a></td>
          <td>說明公司賣什麼、賣給誰、怎麼收費</td>
          <td>SaaS、Vertical SaaS、Horizontal SaaS、CDP、Enterprise License</td>
      </tr>
      <tr>
          <td><a href="/blog/business/knowledge-cards/#%e5%96%ae%e4%bd%8d%e7%b6%93%e6%bf%9f" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">單位經濟</a></td>
          <td>說明每個客戶或每筆交易的成本與利潤結構</td>
          <td>COGS、Gross Margin、Marginal Cost、P&amp;L、Burn Rate、Runway</td>
      </tr>
      <tr>
          <td><a href="/blog/business/knowledge-cards/#%e9%80%b2%e5%85%a5%e5%b8%82%e5%a0%b4" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">進入市場</a></td>
          <td>說明用什麼通路與銷售模式把產品賣出去</td>
          <td>GTM、PLG、FDE、JV、CAC</td>
      </tr>
      <tr>
          <td><a href="/blog/business/knowledge-cards/#%e7%ab%b6%e7%88%ad%e8%ad%b7%e5%9f%8e%e6%b2%b3" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">競爭護城河</a></td>
          <td>說明為什麼客戶留下來、為什麼別人打不進來</td>
          <td>Lock-in、Switching Cost、Retention、Thin Wrapper、Fat Data / Fat Skill、Connector</td>
      </tr>
      <tr>
          <td><a href="/blog/business/knowledge-cards/#%e5%b8%82%e5%a0%b4%e5%8b%95%e6%85%8b" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">市場動態</a></td>
          <td>說明賽道處在什麼階段、競爭強度、需求類型</td>
          <td>Red / Blue Ocean、Consolidation Cycle、Niche Market、High Stickiness、Rigid Demand、Frontier Capability、Distribution</td>
      </tr>
      <tr>
          <td><a href="/blog/business/knowledge-cards/#%e8%b3%87%e6%9c%ac%e4%bc%b0%e5%80%bc" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">資本估值</a></td>
          <td>說明新創 / 公司價值怎麼被定價、被誰定價、何時崩塌</td>
          <td>VC、PE、Valuation、Valuation Compression、Unit Economics、LTV</td>
      </tr>
      <tr>
          <td><a href="/blog/business/knowledge-cards/#%e5%9f%b7%e8%a1%8c%e7%9f%a5%e8%ad%98" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">執行知識</a></td>
          <td>說明把產品做出來、把客戶服務好的隱性能力</td>
          <td>Tacit Knowledge、Evaluation Set、PRD、Wireframe、Vibe Code、Judgment Stake、Junior Buffer</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/business/reading-frameworks/" data-link-title="閱讀商業分析的框架" data-link-desc="幫助讀者識別商業分析文章的讀者定位、寫作目的與可信度，避免誤讀策略分析、產業內幕與大眾財經">閱讀框架</a> 處理「眼前這篇文章是寫給誰看、目的是什麼、該怎麼讀」。看到一篇分析時先用閱讀框架定位文章類型，再用分類卡片解碼術語。<a href="/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解</a> 則是把整個流程實作出來—每篇文章拿一個具體市場事件（例如 Claude for Legal 推出、CoreWeave 收購 Bufstream），用 WRAP 結構走完一遍判讀。</p>
<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>能看懂 SaaS、CAC、PLG 等基本縮寫構成的句子</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>閱讀框架 → 對應卡片 → <a href="/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解</a> 看完整 WRAP 範例</td>
          <td>能識別文章類型、目標讀者、引用的概念與隱含的判斷</td>
      </tr>
      <tr>
          <td>自己寫 WRAP 拆解</td>
          <td>想練習結構化分析市場事件的人</td>
          <td><a href="/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解 _index</a> → 三篇範例 → 套用結構模板</td>
          <td>能用 WRAP 拆任何市場事件、產出可遷移的判讀框架</td>
      </tr>
  </tbody>
</table>
<h2 id="怎麼擴充這個模組">怎麼擴充這個模組</h2>
<p>擴充走兩條路、依內容類型決定。</p>
<h3 id="新術語擴充-knowledge-cards">新術語：擴充 knowledge-cards</h3>
<p>新術語從社群貼文或書中出現時：</p>
<ol>
<li>用 <a href="/blog/business/knowledge-cards/#%e5%bb%ba%e5%8d%a1%e5%88%a4%e6%ba%96" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">建卡判準</a> 判斷該術語是否值得獨立建卡。</li>
<li>用 <a href="#%e5%88%86%e9%a1%9e%e9%ab%94%e7%b3%bb">分類體系</a> 找到該卡片應歸屬的分類。</li>
<li>用 <a href="/blog/business/knowledge-cards/#%e5%8d%a1%e7%89%87%e6%a0%bc%e5%bc%8f" data-link-title="商業概念知識卡片" data-link-desc="用原子化卡片整理商業模式、單位經濟、進入市場、競爭護城河、市場動態、資本估值與執行知識的術語">卡片格式</a> 寫卡，遵循「核心概念、概念位置、可觀察訊號、判讀方式」四段結構。</li>
<li>在 <code>knowledge-cards/_index.md</code> 對應分類表格內加入連結。</li>
</ol>
<p>不適合建卡的術語（過度寬泛、僅是字面翻譯、只能在原文中成立）應在分析文章中直接補清楚，避免建出單薄卡片。</p>
<h3 id="新市場事件擴充-case-analyses">新市場事件：擴充 case-analyses</h3>
<p>看到值得拆解的市場事件（M&amp;A、產品推出、IPO、產業整併、政策變動）時：</p>
<ol>
<li>用 <a href="/blog/business/reading-frameworks/reader-purpose-matrix/" data-link-title="媒介—讀者—目的矩陣" data-link-desc="用媒介、讀者、目的三軸定位一篇商業分析的類型，避免把策略分析誤讀成投資建議或把產業內幕誤讀成大眾財經">媒介—讀者—目的矩陣</a> 先定位原文類型。</li>
<li>用 <a href="/blog/business/case-analyses/" data-link-title="商業案例 WRAP 拆解" data-link-desc="用 WRAP 框架拆解具體市場事件，抽出可遷移的策略判讀框架，不局限於 AI 議題">案例拆解的 WRAP 結構模板</a> 逐段填寫。</li>
<li>確保每個 Widen Option 都有對應 Reality Test、結尾必須給可遷移的判讀框架表。</li>
<li>Tripwire 段必須具體可監控（不能寫「再觀察」這種模糊話）。</li>
</ol>
<p>如果事件無法產出可遷移框架（只是孤立特例）、放筆記裡即可、不要硬寫成案例。</p>
<h2 id="跟其他模組的關係">跟其他模組的關係</h2>
<p>商業教材跟 backend 教材是兩個獨立 surface，互不直接依賴。Backend 教材關心的是「服務能力、操作責任、失敗代價」；商業教材關心的是「公司怎麼賺錢、客戶怎麼留下、估值怎麼成立」。技術選型決策（例如「要不要遷移到 Diskless Kafka」）會同時被兩邊影響—backend 看遷移成本與風險，business 看<a href="/blog/business/knowledge-cards/consolidation-cycle/" data-link-title="Consolidation Cycle" data-link-desc="說明整併週期的階段特徵">整併週期</a> 與<a href="/blog/business/knowledge-cards/gross-margin/" data-link-title="Gross Margin" data-link-desc="說明毛利率與其對商業模式可行性的決定作用">毛利結構</a>—但兩個敘事各自獨立，不互相替代。</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>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>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></channel></rss>