<?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>Decision on Tarragon</title><link>https://tarrragon.github.io/blog/tags/decision/</link><description>Recent content in Decision on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 01 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/decision/index.xml" rel="self" type="application/rss+xml"/><item><title>Mock 邊界判斷決策表</title><link>https://tarrragon.github.io/blog/testing/05-test-design-judgment/mock-boundary-decision/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/05-test-design-judgment/mock-boundary-decision/</guid><description>&lt;p>Mock 的適用範圍由它模擬的層級決定。Mock 忠實模擬 API 層的契約（方法簽名、參數型別），但無法模擬協議層的語意差異和環境層的行為差異。判斷「這個 test 用 mock 夠不夠」的依據是：test 要驗證的行為發生在哪一層。&lt;/p>
&lt;h2 id="決策依據">決策依據&lt;/h2>
&lt;h3 id="mock-夠用的場景">Mock 夠用的場景&lt;/h3>
&lt;p>Test 驗證的行為完全在程式碼內部 — 函式邏輯、狀態機轉換、資料轉換、錯誤處理分支。這些行為不依賴外部服務的協議細節，mock 提供的 API 層模擬已經足夠。&lt;/p>
&lt;p>判斷問題：&lt;strong>如果把 mock 替換成真實服務，test 的斷言結果會不會改變？&lt;/strong> 如果不會改變，mock 夠用。&lt;/p>
&lt;p>例：&lt;code>ConnectionManager&lt;/code> 收到 error 後是否正確切換到 error 狀態 — 不管 error 來自 mock 還是真實 WebSocket，狀態機邏輯相同。Mock 夠用。&lt;/p>
&lt;h3 id="mock-不夠的場景">Mock 不夠的場景&lt;/h3>
&lt;p>Test 要驗證的行為涉及外部服務的協議行為 — frame type 差異、認證流程、編碼格式、逾時行為。Mock 的 API 層模擬跳過了這些行為，test 通過不代表真實互動也通過。&lt;/p>
&lt;p>判斷問題：&lt;strong>Mock 跳過了外部服務的哪些步驟？這些步驟的行為是否影響 test 要驗證的結果？&lt;/strong> 如果是，需要 protocol integration test（&lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三&lt;/a>）。&lt;/p>
&lt;p>例：&lt;code>sendData()&lt;/code> 發送鍵盤輸入 — mock 的 &lt;code>sink.add(dynamic)&lt;/code> 接受任何型別，但真實 &lt;code>IOWebSocketChannel&lt;/code> 對 &lt;code>String&lt;/code> 和 &lt;code>Uint8List&lt;/code> 產生不同 frame type。Mock 不夠。&lt;/p>
&lt;h2 id="決策表">決策表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>驗證對象&lt;/th>
 &lt;th>Mock 夠用？&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>error 來源不影響處理邏輯&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>mock 可能跳過認證步驟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料編碼格式&lt;/td>
 &lt;td>不夠&lt;/td>
 &lt;td>mock 不區分編碼差異（text vs binary）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>逾時行為&lt;/td>
 &lt;td>不夠&lt;/td>
 &lt;td>mock 的回應時間和真實服務不同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多步驟協議流程&lt;/td>
 &lt;td>不夠&lt;/td>
 &lt;td>mock 可能簡化多步驟為單步&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>並行/競爭條件&lt;/td>
 &lt;td>不夠&lt;/td>
 &lt;td>mock 通常同步回應，無法模擬真實的並行行為&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="灰色地帶的判斷">灰色地帶的判斷&lt;/h2>
&lt;p>有些 test 介於「mock 夠用」和「mock 不夠」之間。例如驗證「連線失敗時顯示 error 訊息」— 觸發失敗的方式可以是 mock 回傳 error（驗證顯示邏輯），也可以是真實服務拒絕連線（驗證真實失敗場景的處理）。&lt;/p>
&lt;p>灰色地帶的判斷策略是：用 mock test 驗證「收到 error 後的處理邏輯」，用 protocol integration test 驗證「真實服務在什麼情況下回傳 error」。兩層 test 各自回答不同問題，不互相替代（&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 各層職責、驗證目標與盲區的完整論述">testing 模組一 三層定義&lt;/a>）。&lt;/p>
&lt;p>Mock 邊界確定後，另一個影響 test 有效性的因素是&lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 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 無法也不應該模擬協議行為">testing 模組一 Mock 遮蔽機制分析&lt;/a>中完整展開，判定需要真實服務後的成本評估見 &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/cost-judgment/" data-link-title="成本判斷表" data-link-desc="什麼時候值得寫 protocol integration test、什麼時候用 contract test 或實機測試替代 — 根據服務啟動成本和協議複雜度判斷">testing 模組三 成本判斷表&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Mock 的適用範圍由它模擬的層級決定。Mock 忠實模擬 API 層的契約（方法簽名、參數型別），但無法模擬協議層的語意差異和環境層的行為差異。判斷「這個 test 用 mock 夠不夠」的依據是：test 要驗證的行為發生在哪一層。</p>
<h2 id="決策依據">決策依據</h2>
<h3 id="mock-夠用的場景">Mock 夠用的場景</h3>
<p>Test 驗證的行為完全在程式碼內部 — 函式邏輯、狀態機轉換、資料轉換、錯誤處理分支。這些行為不依賴外部服務的協議細節，mock 提供的 API 層模擬已經足夠。</p>
<p>判斷問題：<strong>如果把 mock 替換成真實服務，test 的斷言結果會不會改變？</strong> 如果不會改變，mock 夠用。</p>
<p>例：<code>ConnectionManager</code> 收到 error 後是否正確切換到 error 狀態 — 不管 error 來自 mock 還是真實 WebSocket，狀態機邏輯相同。Mock 夠用。</p>
<h3 id="mock-不夠的場景">Mock 不夠的場景</h3>
<p>Test 要驗證的行為涉及外部服務的協議行為 — frame type 差異、認證流程、編碼格式、逾時行為。Mock 的 API 層模擬跳過了這些行為，test 通過不代表真實互動也通過。</p>
<p>判斷問題：<strong>Mock 跳過了外部服務的哪些步驟？這些步驟的行為是否影響 test 要驗證的結果？</strong> 如果是，需要 protocol integration test（<a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三</a>）。</p>
<p>例：<code>sendData()</code> 發送鍵盤輸入 — mock 的 <code>sink.add(dynamic)</code> 接受任何型別，但真實 <code>IOWebSocketChannel</code> 對 <code>String</code> 和 <code>Uint8List</code> 產生不同 frame type。Mock 不夠。</p>
<h2 id="決策表">決策表</h2>
<table>
  <thead>
      <tr>
          <th>驗證對象</th>
          <th>Mock 夠用？</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>error 來源不影響處理邏輯</td>
      </tr>
      <tr>
          <td>資料格式轉換</td>
          <td>夠</td>
          <td>轉換邏輯在程式碼內部</td>
      </tr>
      <tr>
          <td>連線建立成功/失敗</td>
          <td>視情況</td>
          <td>如果只驗證「收到成功/失敗後做什麼」→ 夠</td>
      </tr>
      <tr>
          <td>認證流程完整性</td>
          <td>不夠</td>
          <td>mock 可能跳過認證步驟</td>
      </tr>
      <tr>
          <td>資料編碼格式</td>
          <td>不夠</td>
          <td>mock 不區分編碼差異（text vs binary）</td>
      </tr>
      <tr>
          <td>逾時行為</td>
          <td>不夠</td>
          <td>mock 的回應時間和真實服務不同</td>
      </tr>
      <tr>
          <td>多步驟協議流程</td>
          <td>不夠</td>
          <td>mock 可能簡化多步驟為單步</td>
      </tr>
      <tr>
          <td>並行/競爭條件</td>
          <td>不夠</td>
          <td>mock 通常同步回應，無法模擬真實的並行行為</td>
      </tr>
  </tbody>
</table>
<h2 id="灰色地帶的判斷">灰色地帶的判斷</h2>
<p>有些 test 介於「mock 夠用」和「mock 不夠」之間。例如驗證「連線失敗時顯示 error 訊息」— 觸發失敗的方式可以是 mock 回傳 error（驗證顯示邏輯），也可以是真實服務拒絕連線（驗證真實失敗場景的處理）。</p>
<p>灰色地帶的判斷策略是：用 mock test 驗證「收到 error 後的處理邏輯」，用 protocol integration test 驗證「真實服務在什麼情況下回傳 error」。兩層 test 各自回答不同問題，不互相替代（<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 各層職責、驗證目標與盲區的完整論述">testing 模組一 三層定義</a>）。</p>
<p>Mock 邊界確定後，另一個影響 test 有效性的因素是<a href="/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 test 能發現什麼問題">測試資料的代表性</a> — 測試輸入能否反映真實環境。Mock 遮蔽的結構性原因在 <a href="/blog/testing/01-test-strategy-layers/mock-masking-mechanism/" data-link-title="Mock 遮蔽機制分析" data-link-desc="Mock 在 API 層、協議層、環境層之間製造的結構性盲區 — 斷裂點在哪、為什麼 mock 無法也不應該模擬協議行為">testing 模組一 Mock 遮蔽機制分析</a>中完整展開，判定需要真實服務後的成本評估見 <a href="/blog/testing/03-protocol-integration-test/cost-judgment/" data-link-title="成本判斷表" data-link-desc="什麼時候值得寫 protocol integration test、什麼時候用 contract test 或實機測試替代 — 根據服務啟動成本和協議複雜度判斷">testing 模組三 成本判斷表</a>。</p>
]]></content:encoded></item><item><title>自架 vs 商業的判斷決策表</title><link>https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/self-hosted-vs-commercial/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/self-hosted-vs-commercial/</guid><description>&lt;p>自架監控和商業方案之間的選擇取決於四個維度的組合。每個維度有明確的閾值 — 超過閾值時自架的成本開始高於商業方案的訂閱費。&lt;/p>
&lt;h2 id="四個判斷維度">四個判斷維度&lt;/h2>
&lt;h3 id="使用者數">使用者數&lt;/h3>
&lt;p>自架方案的成本和使用者數幾乎無關（JSONL + grep 處理 1 個和 100 個使用者的成本差異很小）。商業方案按事件量或使用者數計費，使用者數增長直接推高費用。&lt;/p>
&lt;p>&lt;strong>經驗估算&lt;/strong>：使用者數在百人以下時，自架的總成本（開發 + 維護 + 硬體）通常低於商業方案的年費（以典型商業方案年費 $300-$600 和自架的開發維護時間估算）。使用者數在千人以上時，自架需要投入的基礎設施維護（高可用、擴容、備份）成本上升，商業方案的規模經濟開始有優勢。具體的交叉點取決於選用的 vendor 定價（Sentry Developer plan 免費額度 5000 events/月、PostHog 免費到 1M events/月）和自架的維護時間成本。&lt;/p>
&lt;p>兩者之間是灰色地帶 — 取決於功能需求和團隊能力。&lt;/p>
&lt;h3 id="網路範圍">網路範圍&lt;/h3>
&lt;p>使用者和 collector 是否在同一個網路內。&lt;/p>
&lt;p>&lt;strong>同一網路&lt;/strong>（自用工具、內部工具）：自架方案直接 HTTP POST 到本機或內網 endpoint，不需要 DNS、TLS 憑證、CDN。成本極低。&lt;/p>
&lt;p>&lt;strong>外部網路&lt;/strong>（公開 app、SaaS）：自架方案需要處理公網暴露、DDoS 防護、TLS 憑證管理、高可用（多區域部署）。商業方案把這些基礎設施問題內化了。&lt;/p>
&lt;h3 id="功能需求">功能需求&lt;/h3>
&lt;p>自架方案的功能上限是開發者願意投入的工程量。grep + jq 能做基礎查詢和 funnel 分析（&lt;a href="https://tarrragon.github.io/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">模組八 自架 funnel&lt;/a>）。Dashboard、告警、session replay、A/B test 分群每個功能都是數週到數月的開發量。&lt;/p>
&lt;p>商業方案的功能開箱即用。如果需求包含 session replay、A/B test dashboard、自動 issue 分群，商業方案的功能完成度遠高於自架。&lt;/p>
&lt;h3 id="合規要求">合規要求&lt;/h3>
&lt;p>資料必須存放在特定地區（GDPR data residency）或不能離開公司網路（金融、醫療）。&lt;/p>
&lt;p>&lt;strong>自架&lt;/strong>：資料完全在自己的基礎設施上，資料位置由自己控制。適合最嚴格的合規要求。&lt;/p>
&lt;p>&lt;strong>商業方案&lt;/strong>：資料存放在 vendor 的基礎設施上。部分 vendor 提供 data residency 選項（Sentry 的 EU hosting、Datadog 的 EU region），但仍然是第三方持有資料。&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>&amp;lt; 100&lt;/td>
 &lt;td>&amp;gt; 1000&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>Dashboard + 告警 + replay&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;p>決策表指向商業方案後，&lt;a href="https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/sentry-deep-dive/" data-link-title="Sentry 深入" data-link-desc="Error tracking &amp;#43; performance monitoring &amp;#43; session replay 的架構 — Sentry 從 error-first 出發如何擴展到全面可觀測性">Sentry 深入&lt;/a>和 &lt;a href="https://tarrragon.github.io/blog/monitoring/06-commercial-comparison/firebase-suite/" data-link-title="Firebase 套件" data-link-desc="Crashlytics &amp;#43; Analytics &amp;#43; Remote Config 的整合 — Firebase 把 error tracking 和行為分析拆成獨立產品的設計取捨">Firebase 套件&lt;/a>分別展開兩個主流方案的架構和能力邊界。決策表指向自架時，&lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計&lt;/a>提供從 HTTP endpoint 到 rule engine 的完整實作藍圖。Server-side 的可觀測性（OTLP、Prometheus、Grafana）見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend 模組四 可觀測性&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>自架監控和商業方案之間的選擇取決於四個維度的組合。每個維度有明確的閾值 — 超過閾值時自架的成本開始高於商業方案的訂閱費。</p>
<h2 id="四個判斷維度">四個判斷維度</h2>
<h3 id="使用者數">使用者數</h3>
<p>自架方案的成本和使用者數幾乎無關（JSONL + grep 處理 1 個和 100 個使用者的成本差異很小）。商業方案按事件量或使用者數計費，使用者數增長直接推高費用。</p>
<p><strong>經驗估算</strong>：使用者數在百人以下時，自架的總成本（開發 + 維護 + 硬體）通常低於商業方案的年費（以典型商業方案年費 $300-$600 和自架的開發維護時間估算）。使用者數在千人以上時，自架需要投入的基礎設施維護（高可用、擴容、備份）成本上升，商業方案的規模經濟開始有優勢。具體的交叉點取決於選用的 vendor 定價（Sentry Developer plan 免費額度 5000 events/月、PostHog 免費到 1M events/月）和自架的維護時間成本。</p>
<p>兩者之間是灰色地帶 — 取決於功能需求和團隊能力。</p>
<h3 id="網路範圍">網路範圍</h3>
<p>使用者和 collector 是否在同一個網路內。</p>
<p><strong>同一網路</strong>（自用工具、內部工具）：自架方案直接 HTTP POST 到本機或內網 endpoint，不需要 DNS、TLS 憑證、CDN。成本極低。</p>
<p><strong>外部網路</strong>（公開 app、SaaS）：自架方案需要處理公網暴露、DDoS 防護、TLS 憑證管理、高可用（多區域部署）。商業方案把這些基礎設施問題內化了。</p>
<h3 id="功能需求">功能需求</h3>
<p>自架方案的功能上限是開發者願意投入的工程量。grep + jq 能做基礎查詢和 funnel 分析（<a href="/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">模組八 自架 funnel</a>）。Dashboard、告警、session replay、A/B test 分群每個功能都是數週到數月的開發量。</p>
<p>商業方案的功能開箱即用。如果需求包含 session replay、A/B test dashboard、自動 issue 分群，商業方案的功能完成度遠高於自架。</p>
<h3 id="合規要求">合規要求</h3>
<p>資料必須存放在特定地區（GDPR data residency）或不能離開公司網路（金融、醫療）。</p>
<p><strong>自架</strong>：資料完全在自己的基礎設施上，資料位置由自己控制。適合最嚴格的合規要求。</p>
<p><strong>商業方案</strong>：資料存放在 vendor 的基礎設施上。部分 vendor 提供 data residency 選項（Sentry 的 EU hosting、Datadog 的 EU region），但仍然是第三方持有資料。</p>
<h2 id="決策表">決策表</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>自架有利</th>
          <th>商業方案有利</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者數</td>
          <td>&lt; 100</td>
          <td>&gt; 1000</td>
      </tr>
      <tr>
          <td>網路範圍</td>
          <td>同一網路</td>
          <td>外部網路</td>
      </tr>
      <tr>
          <td>功能需求</td>
          <td>查詢 + 基礎分析</td>
          <td>Dashboard + 告警 + replay</td>
      </tr>
      <tr>
          <td>合規要求</td>
          <td>資料不能離開自有設施</td>
          <td>無特殊限制</td>
      </tr>
  </tbody>
</table>
<p>四個維度中三個以上指向同一方向 → 選那個方向。兩兩對半 → 從自架開始（成本低、可逆），需求增長後再評估切換。</p>
<p>決策表指向商業方案後，<a href="/blog/monitoring/06-commercial-comparison/sentry-deep-dive/" data-link-title="Sentry 深入" data-link-desc="Error tracking &#43; performance monitoring &#43; session replay 的架構 — Sentry 從 error-first 出發如何擴展到全面可觀測性">Sentry 深入</a>和 <a href="/blog/monitoring/06-commercial-comparison/firebase-suite/" data-link-title="Firebase 套件" data-link-desc="Crashlytics &#43; Analytics &#43; Remote Config 的整合 — Firebase 把 error tracking 和行為分析拆成獨立產品的設計取捨">Firebase 套件</a>分別展開兩個主流方案的架構和能力邊界。決策表指向自架時，<a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計</a>提供從 HTTP endpoint 到 rule engine 的完整實作藍圖。Server-side 的可觀測性（OTLP、Prometheus、Grafana）見 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend 模組四 可觀測性</a>。</p>
<h2 id="中間路線">中間路線</h2>
<p>上表是「完全自架 vs 專業監控 SaaS」的兩端。中間還有兩條路徑 — 用 BaaS（Supabase + Vercel）搭出託管版 collector，或用 PaaS（Railway / Fly.io）跑自架 collector 原始碼但不管 server。APP 上線初期用免費方案零成本起步、保留自訂 schema 彈性是常見的起步策略。完整的四條路徑比較、架構差異、免費方案限額和遷移路線見<a href="/blog/monitoring/06-commercial-comparison/deployment-spectrum/" data-link-title="部署光譜：從 BaaS 到自架的四條路徑" data-link-desc="監控方案的部署選擇不是二元的 — BaaS &#43; Serverless 和 PaaS 是完全自架和商業 SaaS 之間兩條常被忽略的中間路徑">部署光譜</a>。</p>
]]></content:encoded></item><item><title>輸入機制決策表</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/</guid><description>&lt;p>輸入機制是設計產物，在功能規格階段決定，和 API schema、畫面狀態矩陣同級。手機鍵盤的行為由多個參數控制，每個參數都是一個設計決策，影響使用者體驗、UI layout 和通訊協議。&lt;/p>
&lt;h2 id="四個決策維度">四個決策維度&lt;/h2>
&lt;h3 id="keyboard-type顯示哪種鍵盤">Keyboard type：顯示哪種鍵盤&lt;/h3>
&lt;p>Keyboard type 決定使用者按下輸入框時出現什麼鍵盤。數字鍵盤、email 鍵盤、URL 鍵盤、一般文字鍵盤 — 每種鍵盤的按鍵配置和自動行為不同。&lt;/p>
&lt;p>選擇判斷依據是「使用者要輸入什麼內容」。email 地址用 email 鍵盤（有 &lt;code>@&lt;/code> 鍵），電話號碼用數字鍵盤，密碼或 CLI 指令用 &lt;code>visiblePassword&lt;/code> 型別（避免自動校正和建議）。&lt;/p>
&lt;p>app_tunnel 的 terminal 輸入框用 &lt;code>TextInputType.visiblePassword&lt;/code> — 因為 CLI 指令包含路徑分隔符、flag 縮寫等非自然語言內容，一般文字鍵盤會嘗試自動校正 &lt;code>ls -la&lt;/code> 或 &lt;code>/usr/bin/&lt;/code> 成其他東西（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>）。&lt;/p>
&lt;h3 id="submit-model怎麼送出輸入">Submit model：怎麼送出輸入&lt;/h3>
&lt;p>Submit model 決定使用者輸入的內容何時傳送給系統。兩個基本選項：整行送出（使用者按 Enter/Send 後一次傳送整行）和逐字元送出（每個按鍵即時傳送）。&lt;/p>
&lt;p>這個決策直接影響通訊協議設計（本章合成，UF-8 Derive）。整行送出代表每次傳送一個完整指令字串（&lt;code>ls -la\n&lt;/code>），server 端按行處理。逐字元送出代表每個按鍵產生一個 WebSocket frame（&lt;code>l&lt;/code>、&lt;code>s&lt;/code>、&lt;code> &lt;/code>、&lt;code>-&lt;/code>、&lt;code>l&lt;/code>、&lt;code>a&lt;/code>），server 端需要處理單字元輸入，包括 Tab 補全和 Ctrl+C 這類立即回應的操作。&lt;/p>
&lt;p>app_tunnel 選擇整行送出（&lt;code>onSubmitted&lt;/code>），代表 Tab 補全在 client 端無法觸發（因為 Tab 不會單獨送出），但實作成本較低且協議設計較簡單。逐字元送出支援 Tab 補全和命令編輯，但 protocol 複雜度顯著提高。&lt;/p>
&lt;h3 id="ime-policy輸入法的行為控制">IME policy：輸入法的行為控制&lt;/h3>
&lt;p>IME（Input Method Editor）policy 控制手機輸入法的自動行為：自動校正、建議列、個人化學習。每個行為在某些輸入場景是幫助，在另一些場景是干擾或安全風險。&lt;/p>
&lt;p>三個控制項各自有獨立的影響：&lt;/p>
&lt;ul>
&lt;li>&lt;code>autocorrect&lt;/code>：自動校正把輸入替換成字典中的詞。CLI 指令和路徑不是自然語言，自動校正會破壞輸入內容。&lt;/li>
&lt;li>&lt;code>enableSuggestions&lt;/code>：建議列在鍵盤上方顯示候選詞。在 terminal 場景中建議列遮擋畫面底部的終端機輸出。&lt;/li>
&lt;li>&lt;code>enableIMEPersonalizedLearning&lt;/code>：IME 從使用者輸入中學習新詞，跨 app 適用。CLI 輸入可能包含密碼和路徑 — 這是安全問題，見 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制 checklist&lt;/a>。&lt;/li>
&lt;/ul>
&lt;h3 id="special-keys特殊按鍵的處理">Special keys：特殊按鍵的處理&lt;/h3>
&lt;p>手機鍵盤沒有桌面鍵盤的 Esc、Tab、Ctrl、方向鍵。如果應用需要這些按鍵，必須自建 UI 元件提供。&lt;/p>
&lt;p>app_tunnel 用底部工具列提供 Esc/Tab/Ctrl/方向鍵。這個工具列的設計（按鈕大小、排列、長按行為）是 UX 決策，不是實作細節。&lt;/p>
&lt;h2 id="決策表作為設計產物">決策表作為設計產物&lt;/h2>
&lt;p>四個維度的決策應該在功能規格中以表格形式記錄，讓 code review 時可以逐項對照實作是否符合規格。&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>Keyboard&lt;/td>
 &lt;td>visiblePassword&lt;/td>
 &lt;td>CLI 指令不適用自動校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Submit&lt;/td>
 &lt;td>整行送出&lt;/td>
 &lt;td>protocol 簡單，犧牲 Tab 補全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IME&lt;/td>
 &lt;td>全關&lt;/td>
 &lt;td>安全考量 + 非自然語言輸入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Special keys&lt;/td>
 &lt;td>底部工具列&lt;/td>
 &lt;td>手機無實體 Esc/Tab/Ctrl&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>app_tunnel 的六個 TextField 參數全是 W2 hotfix 事後補上的，沒有一個是事前規劃。每個參數都有 gotcha — 漏掉 &lt;code>enableIMEPersonalizedLearning: false&lt;/code> 就是安全漏洞，漏掉 &lt;code>autocorrect: false&lt;/code> 就是 UX 問題。事先決策並記錄在規格中，code review 時逐項勾選，比事後逐一發現問題的成本低。&lt;/p>
&lt;p>四個維度在不同場景下的具體決策各有不同。CLI 場景的特殊需求見 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計&lt;/a>，安全敏感欄位的 IME 控制逐項列在 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist&lt;/a>。Submit model 的選擇（整行 vs 逐字元）直接影響通訊協議的設計 — 這個交叉影響在 &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試&lt;/a>中從 test 的角度分析。&lt;/p></description><content:encoded><![CDATA[<p>輸入機制是設計產物，在功能規格階段決定，和 API schema、畫面狀態矩陣同級。手機鍵盤的行為由多個參數控制，每個參數都是一個設計決策，影響使用者體驗、UI layout 和通訊協議。</p>
<h2 id="四個決策維度">四個決策維度</h2>
<h3 id="keyboard-type顯示哪種鍵盤">Keyboard type：顯示哪種鍵盤</h3>
<p>Keyboard type 決定使用者按下輸入框時出現什麼鍵盤。數字鍵盤、email 鍵盤、URL 鍵盤、一般文字鍵盤 — 每種鍵盤的按鍵配置和自動行為不同。</p>
<p>選擇判斷依據是「使用者要輸入什麼內容」。email 地址用 email 鍵盤（有 <code>@</code> 鍵），電話號碼用數字鍵盤，密碼或 CLI 指令用 <code>visiblePassword</code> 型別（避免自動校正和建議）。</p>
<p>app_tunnel 的 terminal 輸入框用 <code>TextInputType.visiblePassword</code> — 因為 CLI 指令包含路徑分隔符、flag 縮寫等非自然語言內容，一般文字鍵盤會嘗試自動校正 <code>ls -la</code> 或 <code>/usr/bin/</code> 成其他東西（<a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a>）。</p>
<h3 id="submit-model怎麼送出輸入">Submit model：怎麼送出輸入</h3>
<p>Submit model 決定使用者輸入的內容何時傳送給系統。兩個基本選項：整行送出（使用者按 Enter/Send 後一次傳送整行）和逐字元送出（每個按鍵即時傳送）。</p>
<p>這個決策直接影響通訊協議設計（本章合成，UF-8 Derive）。整行送出代表每次傳送一個完整指令字串（<code>ls -la\n</code>），server 端按行處理。逐字元送出代表每個按鍵產生一個 WebSocket frame（<code>l</code>、<code>s</code>、<code> </code>、<code>-</code>、<code>l</code>、<code>a</code>），server 端需要處理單字元輸入，包括 Tab 補全和 Ctrl+C 這類立即回應的操作。</p>
<p>app_tunnel 選擇整行送出（<code>onSubmitted</code>），代表 Tab 補全在 client 端無法觸發（因為 Tab 不會單獨送出），但實作成本較低且協議設計較簡單。逐字元送出支援 Tab 補全和命令編輯，但 protocol 複雜度顯著提高。</p>
<h3 id="ime-policy輸入法的行為控制">IME policy：輸入法的行為控制</h3>
<p>IME（Input Method Editor）policy 控制手機輸入法的自動行為：自動校正、建議列、個人化學習。每個行為在某些輸入場景是幫助，在另一些場景是干擾或安全風險。</p>
<p>三個控制項各自有獨立的影響：</p>
<ul>
<li><code>autocorrect</code>：自動校正把輸入替換成字典中的詞。CLI 指令和路徑不是自然語言，自動校正會破壞輸入內容。</li>
<li><code>enableSuggestions</code>：建議列在鍵盤上方顯示候選詞。在 terminal 場景中建議列遮擋畫面底部的終端機輸出。</li>
<li><code>enableIMEPersonalizedLearning</code>：IME 從使用者輸入中學習新詞，跨 app 適用。CLI 輸入可能包含密碼和路徑 — 這是安全問題，見 <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制 checklist</a>。</li>
</ul>
<h3 id="special-keys特殊按鍵的處理">Special keys：特殊按鍵的處理</h3>
<p>手機鍵盤沒有桌面鍵盤的 Esc、Tab、Ctrl、方向鍵。如果應用需要這些按鍵，必須自建 UI 元件提供。</p>
<p>app_tunnel 用底部工具列提供 Esc/Tab/Ctrl/方向鍵。這個工具列的設計（按鈕大小、排列、長按行為）是 UX 決策，不是實作細節。</p>
<h2 id="決策表作為設計產物">決策表作為設計產物</h2>
<p>四個維度的決策應該在功能規格中以表格形式記錄，讓 code review 時可以逐項對照實作是否符合規格。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>選項</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Keyboard</td>
          <td>visiblePassword</td>
          <td>CLI 指令不適用自動校正</td>
      </tr>
      <tr>
          <td>Submit</td>
          <td>整行送出</td>
          <td>protocol 簡單，犧牲 Tab 補全</td>
      </tr>
      <tr>
          <td>IME</td>
          <td>全關</td>
          <td>安全考量 + 非自然語言輸入</td>
      </tr>
      <tr>
          <td>Special keys</td>
          <td>底部工具列</td>
          <td>手機無實體 Esc/Tab/Ctrl</td>
      </tr>
  </tbody>
</table>
<p>app_tunnel 的六個 TextField 參數全是 W2 hotfix 事後補上的，沒有一個是事前規劃。每個參數都有 gotcha — 漏掉 <code>enableIMEPersonalizedLearning: false</code> 就是安全漏洞，漏掉 <code>autocorrect: false</code> 就是 UX 問題。事先決策並記錄在規格中，code review 時逐項勾選，比事後逐一發現問題的成本低。</p>
<p>四個維度在不同場景下的具體決策各有不同。CLI 場景的特殊需求見 <a href="/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計</a>，安全敏感欄位的 IME 控制逐項列在 <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist</a>。Submit model 的選擇（整行 vs 逐字元）直接影響通訊協議的設計 — 這個交叉影響在 <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a>中從 test 的角度分析。</p>
]]></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>成本判斷表</title><link>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/cost-judgment/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/cost-judgment/</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;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock 遮蔽" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽&lt;/a>的協議層盲區。但它有建置成本（服務 fixture 管理）和維護成本（服務更新時 test 要跟著改）。判斷是否值得投資，依據的是兩個維度：服務啟動成本和協議複雜度。&lt;/p>
&lt;h2 id="服務啟動成本">服務啟動成本&lt;/h2>
&lt;p>服務啟動成本決定了 protocol integration test 的執行成本 — test 跑一次要多久、CI 中佔多少時間。&lt;/p>
&lt;h3 id="極低成本同機單程序">極低成本（同機單程序）&lt;/h3>
&lt;p>Server 是一個本機程序，&lt;code>Process.start&lt;/code> 一行啟動，不需要 Docker、不需要網路、不需要設定檔。啟動到 ready 不到 2 秒。&lt;/p>
&lt;p>app_tunnel 的 ttyd 就是這個場景。&lt;code>ttyd bash&lt;/code> 在本機啟動，WebSocket 服務立即可用。整個 protocol integration test suite 的額外成本約 10-15 秒（包含啟動、健康檢查、5 個 test 各 2 秒）（本章合成，TF-8 Derive）。&lt;/p>
&lt;p>在這個成本等級下，protocol integration test 幾乎沒有理由不寫。&lt;/p>
&lt;h3 id="低成本docker-單容器">低成本（Docker 單容器）&lt;/h3>
&lt;p>Server 用 Docker 容器啟動，需要 pull image（首次或更新時），啟動到 ready 約 5-30 秒。Redis、PostgreSQL、Elasticsearch 等 open source 服務屬於這個等級。&lt;/p>
&lt;p>CI 中用 image cache 可以把 pull 時間降到接近零。但容器啟動時間仍比原生程序長。整個 protocol integration test suite 的額外成本約 30-60 秒。&lt;/p>
&lt;p>在這個成本等級下，如果協議有任何複雜度（見下方），protocol integration test 值得寫。&lt;/p>
&lt;h3 id="中等成本多容器堆疊">中等成本（多容器堆疊）&lt;/h3>
&lt;p>Server 依賴多個服務（app server + database + cache + message queue），需要 Docker Compose 管理。啟動到所有服務 ready 約 30-120 秒。&lt;/p>
&lt;p>Protocol integration test 的執行成本顯著上升。適合在 CI 的獨立 stage 跑（和 unit test 分開），避免拖慢 fast feedback loop。&lt;/p>
&lt;h3 id="高成本外部服務--saas">高成本（外部服務 / SaaS）&lt;/h3>
&lt;p>Server 是外部 SaaS（Stripe API、AWS S3、第三方 OAuth provider），無法本地啟動。Test 需要打到 sandbox environment，有速率限制和網路延遲。&lt;/p>
&lt;p>在這個成本等級下，consumer-driven contract test 可能比 protocol integration test 更實用 — 用 contract 定義期望的 request/response，在本地驗證 client 端行為，不需要每次都打到外部服務。&lt;/p>
&lt;h2 id="協議複雜度">協議複雜度&lt;/h2>
&lt;p>協議複雜度決定了 mock 遮蔽的風險大小 — 風險越大，protocol integration test 的價值越高。&lt;/p>
&lt;p>&lt;strong>高複雜度&lt;/strong>：WebSocket（frame type、handshake、子協議）、gRPC（streaming、deadline、metadata）、MQTT（QoS level、retain、will message）。API 簽名隱藏了協議層的行為分支，mock 結構性地無法覆蓋。&lt;/p>
&lt;p>&lt;strong>中複雜度&lt;/strong>：HTTP REST API（多種 status code、error body 格式、認證流程、分頁）。核心語意（JSON request/response）差距小，但 edge case（error response 格式、header 要求）仍可能被 mock 遮蔽。&lt;/p>
&lt;p>&lt;strong>低複雜度&lt;/strong>：本地 IPC（Unix socket、named pipe）、標準格式的檔案讀寫。協議行為簡單，mock 和真實行為差距小。&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>protocol test&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>可選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>視 mock 寬鬆度決定&lt;/td>
 &lt;td>實機測試替代&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>contract test + 實機&lt;/td>
 &lt;td>contract test&lt;/td>
 &lt;td>實機測試替代&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「可選」代表 protocol integration test 有價值但不是必要 — 實機測試階段的手動驗證可能足夠。「實機測試替代」代表成本太高或收益太低，依賴實機測試階段的人工驗證。&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> 的價值在於用自動化方式驗證 <a href="/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock 遮蔽" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽</a>的協議層盲區。但它有建置成本（服務 fixture 管理）和維護成本（服務更新時 test 要跟著改）。判斷是否值得投資，依據的是兩個維度：服務啟動成本和協議複雜度。</p>
<h2 id="服務啟動成本">服務啟動成本</h2>
<p>服務啟動成本決定了 protocol integration test 的執行成本 — test 跑一次要多久、CI 中佔多少時間。</p>
<h3 id="極低成本同機單程序">極低成本（同機單程序）</h3>
<p>Server 是一個本機程序，<code>Process.start</code> 一行啟動，不需要 Docker、不需要網路、不需要設定檔。啟動到 ready 不到 2 秒。</p>
<p>app_tunnel 的 ttyd 就是這個場景。<code>ttyd bash</code> 在本機啟動，WebSocket 服務立即可用。整個 protocol integration test suite 的額外成本約 10-15 秒（包含啟動、健康檢查、5 個 test 各 2 秒）（本章合成，TF-8 Derive）。</p>
<p>在這個成本等級下，protocol integration test 幾乎沒有理由不寫。</p>
<h3 id="低成本docker-單容器">低成本（Docker 單容器）</h3>
<p>Server 用 Docker 容器啟動，需要 pull image（首次或更新時），啟動到 ready 約 5-30 秒。Redis、PostgreSQL、Elasticsearch 等 open source 服務屬於這個等級。</p>
<p>CI 中用 image cache 可以把 pull 時間降到接近零。但容器啟動時間仍比原生程序長。整個 protocol integration test suite 的額外成本約 30-60 秒。</p>
<p>在這個成本等級下，如果協議有任何複雜度（見下方），protocol integration test 值得寫。</p>
<h3 id="中等成本多容器堆疊">中等成本（多容器堆疊）</h3>
<p>Server 依賴多個服務（app server + database + cache + message queue），需要 Docker Compose 管理。啟動到所有服務 ready 約 30-120 秒。</p>
<p>Protocol integration test 的執行成本顯著上升。適合在 CI 的獨立 stage 跑（和 unit test 分開），避免拖慢 fast feedback loop。</p>
<h3 id="高成本外部服務--saas">高成本（外部服務 / SaaS）</h3>
<p>Server 是外部 SaaS（Stripe API、AWS S3、第三方 OAuth provider），無法本地啟動。Test 需要打到 sandbox environment，有速率限制和網路延遲。</p>
<p>在這個成本等級下，consumer-driven contract test 可能比 protocol integration test 更實用 — 用 contract 定義期望的 request/response，在本地驗證 client 端行為，不需要每次都打到外部服務。</p>
<h2 id="協議複雜度">協議複雜度</h2>
<p>協議複雜度決定了 mock 遮蔽的風險大小 — 風險越大，protocol integration test 的價值越高。</p>
<p><strong>高複雜度</strong>：WebSocket（frame type、handshake、子協議）、gRPC（streaming、deadline、metadata）、MQTT（QoS level、retain、will message）。API 簽名隱藏了協議層的行為分支，mock 結構性地無法覆蓋。</p>
<p><strong>中複雜度</strong>：HTTP REST API（多種 status code、error body 格式、認證流程、分頁）。核心語意（JSON request/response）差距小，但 edge case（error response 格式、header 要求）仍可能被 mock 遮蔽。</p>
<p><strong>低複雜度</strong>：本地 IPC（Unix socket、named pipe）、標準格式的檔案讀寫。協議行為簡單，mock 和真實行為差距小。</p>
<h2 id="判斷矩陣">判斷矩陣</h2>
<table>
  <thead>
      <tr>
          <th>服務啟動成本</th>
          <th>協議複雜度高</th>
          <th>協議複雜度中</th>
          <th>協議複雜度低</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>極低</td>
          <td>protocol test</td>
          <td>protocol test</td>
          <td>protocol test</td>
      </tr>
      <tr>
          <td>低</td>
          <td>protocol test</td>
          <td>protocol test</td>
          <td>可選</td>
      </tr>
      <tr>
          <td>中</td>
          <td>protocol test</td>
          <td>視 mock 寬鬆度決定</td>
          <td>實機測試替代</td>
      </tr>
      <tr>
          <td>高</td>
          <td>contract test + 實機</td>
          <td>contract test</td>
          <td>實機測試替代</td>
      </tr>
  </tbody>
</table>
<p>「可選」代表 protocol integration test 有價值但不是必要 — 實機測試階段的手動驗證可能足夠。「實機測試替代」代表成本太高或收益太低，依賴實機測試階段的人工驗證。</p>
<p>成本和複雜度的評估結果決定了要建什麼等級的 test 基礎設施。<a href="/blog/testing/03-protocol-integration-test/definition-and-boundary/" data-link-title="Protocol integration test 定義" data-link-desc="Protocol integration test 和 unit test / E2E test 的邊界 — 驗證程式碼和真實服務的協議契約，不驗證 UI 也不用 mock">Protocol integration test 定義</a>提供這一層 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 寬鬆度、失敗靜默度三個維度">testing 模組一的判斷原則</a>從 mock 遮蔽角度補充另一個判斷維度。決定要建之後，<a href="/blog/testing/03-protocol-integration-test/service-fixture-management/" data-link-title="CI 中的服務 fixture 管理" data-link-desc="在 CI 中啟動和停止真實服務的 test harness 設計 — Process.start / Docker / testcontainers 三種方案的適用場景">CI 中的服務 fixture 管理</a>處理啟動和停止真實服務的工程問題。</p>
]]></content:encoded></item><item><title>整合式 Shell vs 手動拼裝：實測足跡、失敗半徑與選型判準</title><link>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/integrated-shell-vs-manual-assembly/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/integrated-shell-vs-manual-assembly/</guid><description>&lt;p>整合式桌面 shell 與手動拼裝，是「一個大程式包辦整個桌面」與「多個小程式各司其職、由 compositor 黏起來」兩種架構。&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&amp;#43;wofi&amp;#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽&lt;/a> 從概念層談過它的取捨（設計鎖定、穩定性風險）；這篇補上在同一台機器上實際跑過兩種之後量到的數據——資源足跡、失敗半徑、配色一致性——把「感覺整合比較方便」變成可以拿數字判斷的選型。&lt;/p>
&lt;p>這裡的數據來自一次 VM 實測：先手動拼裝一套 waybar + wofi + mako + hyprlock，再換成 Caelestia，量兩者的安裝大小、記憶體、config 結構與失敗行為。&lt;/p>
&lt;h2 id="資源足跡差約一個數量級">資源足跡：差約一個數量級&lt;/h2>
&lt;p>整合式 shell 把整個桌面畫在一個程式裡，這個程式通常是重量級的 UI runtime。Caelestia 建在 Quickshell（Qt6/QML）上，實測安裝足跡如下：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>整合式（Caelestia）&lt;/th>
 &lt;th>手動拼裝（waybar+wofi+mako+hyprlock）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>安裝大小&lt;/td>
 &lt;td>約 230 MB（Quickshell 佔 213 MB）&lt;/td>
 &lt;td>約 4.5 MB（waybar 3 MB，其餘 KB 級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>執行記憶體 RSS&lt;/td>
 &lt;td>單一 &lt;code>qs&lt;/code> 程式約 400 MB&lt;/td>
 &lt;td>waybar 約 53 MB + 通知/啟動器（小）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>差距的來源是 Quickshell 這個 UI 框架——那 213 MB 是 &lt;code>quickshell&lt;/code> 套件本身的安裝大小（一個建在 Qt6 上的 QML shell runtime），不是 Caelestia 的功能程式碼；而且 Qt6 的函式庫（&lt;code>qt6-declarative&lt;/code>、&lt;code>qt6-base&lt;/code> 等）還是它之上的額外相依，沒算進這 213 MB。手動拼裝的 waybar、wofi、mako 都是輕量的 wlroots/GTK 程式，加起來還不到 5 MB。&lt;/p>
&lt;p>這一軸在資源受限的機器上才會咬人：舊筆電、記憶體小的 VPS、或你本來就想把桌面壓到最輕。在一台記憶體充裕的桌機上，400 MB 對 60 MB 的差別多半感覺不到；在一台 2 GB RAM 的機器上，這就是「桌面吃掉五分之一記憶體」跟「幾乎不佔」的差別。&lt;/p>
&lt;h2 id="失敗半徑單點-vs-各自獨立">失敗半徑：單點 vs 各自獨立&lt;/h2>
&lt;p>整合式 shell 把狀態列、通知、鎖屏、啟動器畫在&lt;strong>同一個程式&lt;/strong>裡，所以這個程式崩潰時，這些東西會&lt;strong>一起消失&lt;/strong>。手動拼裝的每個元件是獨立行程，一個崩掉不影響其他——mako（通知）崩了，waybar（狀態列）還在。&lt;/p>
&lt;p>這不只是理論。這次 VM 實測就撞到一個具體案例：Caelestia 的鎖屏是由 Quickshell 主程式畫的，當這個持鎖的程式被中止時，Hyprland 依 &lt;code>ext-session-lock&lt;/code> 協議保持鎖定並顯示「lockscreen app died」的死局——狀態列、通知、鎖屏因為同源，一個環節出事就連帶整個桌面 UI。手動拼裝的 hyprlock 是獨立的鎖屏程式，它崩潰同樣會觸發那個死局，但你的狀態列與通知不會跟著沒。&lt;/p>
&lt;p>這一軸在穩定性敏感或無人值守的場景最關鍵。跑長時間無人盯著的任務時，「一個元件崩掉只損失那個元件」的隔離性，比「全部整合在一起」的一致性更值錢——因為沒人在旁邊立刻重啟。&lt;/p>
&lt;h2 id="配色一致性最容易被低估的一軸">配色一致性：最容易被低估的一軸&lt;/h2>
&lt;p>讓整個桌面配色一致，是整合式與手動拼裝差別最大、卻最常被忽略的地方。整合式 shell 因為所有元件在同一個程式裡，天生共用一套配色——Caelestia 的 dynamic scheme 從桌布抽一組 Material-3 palette，狀態列、通知、鎖屏、dashboard 全部同時套用，換張桌布整套 UI 跟著變。&lt;/p>
&lt;p>手動拼裝要達到同樣的一致，得自己解決一個跨程式的問題：每個元件用不同的設定格式與主題引擎，它們之間不會自動共享顏色。這次手動拼裝那套時就撞到這點——waybar 的 GTK CSS 引擎讀不到 Hyprland 的 &lt;code>$&lt;/code> 顏色變數，結果 waybar 的 &lt;code>style.css&lt;/code> 裡得&lt;strong>手抄一份跟 Hyprland &lt;code>colors.conf&lt;/code> 相同的 hex 色碼&lt;/strong>。換一次配色，就要在 waybar CSS、wofi CSS、mako config、hyprland colors 好幾個地方各改一遍。&lt;/p>
&lt;p>解這個手工問題的標準做法，是加一層&lt;strong>模板工具&lt;/strong>（matugen、pywal、wallust 之類）：從一張桌布或一套色票，自動生成每個元件的設定檔（例如 &lt;code>matugen/templates/rofi-colors.rasi&lt;/code> 就是給 rofi 用的顏色模板）。這等於是手動重建 Caelestia 內建的那套 dynamic theming pipeline。所以配色一致這件事的真正取捨是：Caelestia 開箱就有「換桌布全套跟著變」，手動拼裝要嘛手抄 hex、要嘛自己搭一條 templating pipeline。&lt;/p>
&lt;h2 id="config-結構">config 結構&lt;/h2>
&lt;p>配色一致的差別，也反映在 config 的形狀上。Caelestia 的使用者設定集中在一個 &lt;code>shell.json&lt;/code>（實測約 24 行就涵蓋狀態列、通知、idle 行為）。手動拼裝的設定散在各元件目錄、各用各的格式：waybar 的 &lt;code>config.jsonc&lt;/code> + &lt;code>style.css&lt;/code>、wofi 的 &lt;code>config&lt;/code> + &lt;code>style.css&lt;/code>、mako 的 &lt;code>config&lt;/code>、hypr 的數個 &lt;code>.conf&lt;/code>。集中的好處是好懂好改；散開的好處是每個元件可以獨立替換（把 waybar 換成 ironbar 不影響其他），代價是你要管更多檔案、更多格式。&lt;/p></description><content:encoded><![CDATA[<p>整合式桌面 shell 與手動拼裝，是「一個大程式包辦整個桌面」與「多個小程式各司其職、由 compositor 黏起來」兩種架構。<a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽</a> 從概念層談過它的取捨（設計鎖定、穩定性風險）；這篇補上在同一台機器上實際跑過兩種之後量到的數據——資源足跡、失敗半徑、配色一致性——把「感覺整合比較方便」變成可以拿數字判斷的選型。</p>
<p>這裡的數據來自一次 VM 實測：先手動拼裝一套 waybar + wofi + mako + hyprlock，再換成 Caelestia，量兩者的安裝大小、記憶體、config 結構與失敗行為。</p>
<h2 id="資源足跡差約一個數量級">資源足跡：差約一個數量級</h2>
<p>整合式 shell 把整個桌面畫在一個程式裡，這個程式通常是重量級的 UI runtime。Caelestia 建在 Quickshell（Qt6/QML）上，實測安裝足跡如下：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>整合式（Caelestia）</th>
          <th>手動拼裝（waybar+wofi+mako+hyprlock）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>安裝大小</td>
          <td>約 230 MB（Quickshell 佔 213 MB）</td>
          <td>約 4.5 MB（waybar 3 MB，其餘 KB 級）</td>
      </tr>
      <tr>
          <td>執行記憶體 RSS</td>
          <td>單一 <code>qs</code> 程式約 400 MB</td>
          <td>waybar 約 53 MB + 通知/啟動器（小）</td>
      </tr>
  </tbody>
</table>
<p>差距的來源是 Quickshell 這個 UI 框架——那 213 MB 是 <code>quickshell</code> 套件本身的安裝大小（一個建在 Qt6 上的 QML shell runtime），不是 Caelestia 的功能程式碼；而且 Qt6 的函式庫（<code>qt6-declarative</code>、<code>qt6-base</code> 等）還是它之上的額外相依，沒算進這 213 MB。手動拼裝的 waybar、wofi、mako 都是輕量的 wlroots/GTK 程式，加起來還不到 5 MB。</p>
<p>這一軸在資源受限的機器上才會咬人：舊筆電、記憶體小的 VPS、或你本來就想把桌面壓到最輕。在一台記憶體充裕的桌機上，400 MB 對 60 MB 的差別多半感覺不到；在一台 2 GB RAM 的機器上，這就是「桌面吃掉五分之一記憶體」跟「幾乎不佔」的差別。</p>
<h2 id="失敗半徑單點-vs-各自獨立">失敗半徑：單點 vs 各自獨立</h2>
<p>整合式 shell 把狀態列、通知、鎖屏、啟動器畫在<strong>同一個程式</strong>裡，所以這個程式崩潰時，這些東西會<strong>一起消失</strong>。手動拼裝的每個元件是獨立行程，一個崩掉不影響其他——mako（通知）崩了，waybar（狀態列）還在。</p>
<p>這不只是理論。這次 VM 實測就撞到一個具體案例：Caelestia 的鎖屏是由 Quickshell 主程式畫的，當這個持鎖的程式被中止時，Hyprland 依 <code>ext-session-lock</code> 協議保持鎖定並顯示「lockscreen app died」的死局——狀態列、通知、鎖屏因為同源，一個環節出事就連帶整個桌面 UI。手動拼裝的 hyprlock 是獨立的鎖屏程式，它崩潰同樣會觸發那個死局，但你的狀態列與通知不會跟著沒。</p>
<p>這一軸在穩定性敏感或無人值守的場景最關鍵。跑長時間無人盯著的任務時，「一個元件崩掉只損失那個元件」的隔離性，比「全部整合在一起」的一致性更值錢——因為沒人在旁邊立刻重啟。</p>
<h2 id="配色一致性最容易被低估的一軸">配色一致性：最容易被低估的一軸</h2>
<p>讓整個桌面配色一致，是整合式與手動拼裝差別最大、卻最常被忽略的地方。整合式 shell 因為所有元件在同一個程式裡，天生共用一套配色——Caelestia 的 dynamic scheme 從桌布抽一組 Material-3 palette，狀態列、通知、鎖屏、dashboard 全部同時套用，換張桌布整套 UI 跟著變。</p>
<p>手動拼裝要達到同樣的一致，得自己解決一個跨程式的問題：每個元件用不同的設定格式與主題引擎，它們之間不會自動共享顏色。這次手動拼裝那套時就撞到這點——waybar 的 GTK CSS 引擎讀不到 Hyprland 的 <code>$</code> 顏色變數，結果 waybar 的 <code>style.css</code> 裡得<strong>手抄一份跟 Hyprland <code>colors.conf</code> 相同的 hex 色碼</strong>。換一次配色，就要在 waybar CSS、wofi CSS、mako config、hyprland colors 好幾個地方各改一遍。</p>
<p>解這個手工問題的標準做法，是加一層<strong>模板工具</strong>（matugen、pywal、wallust 之類）：從一張桌布或一套色票，自動生成每個元件的設定檔（例如 <code>matugen/templates/rofi-colors.rasi</code> 就是給 rofi 用的顏色模板）。這等於是手動重建 Caelestia 內建的那套 dynamic theming pipeline。所以配色一致這件事的真正取捨是：Caelestia 開箱就有「換桌布全套跟著變」，手動拼裝要嘛手抄 hex、要嘛自己搭一條 templating pipeline。</p>
<h2 id="config-結構">config 結構</h2>
<p>配色一致的差別，也反映在 config 的形狀上。Caelestia 的使用者設定集中在一個 <code>shell.json</code>（實測約 24 行就涵蓋狀態列、通知、idle 行為）。手動拼裝的設定散在各元件目錄、各用各的格式：waybar 的 <code>config.jsonc</code> + <code>style.css</code>、wofi 的 <code>config</code> + <code>style.css</code>、mako 的 <code>config</code>、hypr 的數個 <code>.conf</code>。集中的好處是好懂好改；散開的好處是每個元件可以獨立替換（把 waybar 換成 ironbar 不影響其他），代價是你要管更多檔案、更多格式。</p>
<h2 id="選型判準">選型判準</h2>
<p>沒有一種在所有軸上都贏。依你的情境對照：</p>
<table>
  <thead>
      <tr>
          <th>你的情境</th>
          <th>偏向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資源受限（舊機、小 RAM VPS）</td>
          <td>手動拼裝（省下那 ~340 MB 記憶體）</td>
      </tr>
      <tr>
          <td>想要開箱即用、換桌布全套變色</td>
          <td>整合式（Caelestia 的 dynamic 原生就有）</td>
      </tr>
      <tr>
          <td>穩定性敏感、無人值守</td>
          <td>手動拼裝（元件獨立、失敗半徑小）</td>
      </tr>
      <tr>
          <td>想要結構性客製（狀態列位置、換 launcher）</td>
          <td>手動拼裝（整合式的結構是 shell 決定的）</td>
      </tr>
      <tr>
          <td>想少管檔案、快速有一套設計一致的成品</td>
          <td>整合式（一個 config、一套配色）</td>
      </tr>
      <tr>
          <td>已經在跑 templating 工具（matugen/pywal）</td>
          <td>手動拼裝（你已經有一致配色的機制、少了整合式的理由）</td>
      </tr>
  </tbody>
</table>
<h3 id="重新評估的訊號tripwire">重新評估的訊號（tripwire）</h3>
<p>選了之後，出現這些訊號時值得回頭重新評估：</p>
<ul>
<li>選了整合式，卻發現一直在跟它的設計決策對抗（想改的結構它不讓你改）——你要的其實是手動拼裝的自由度。</li>
<li>選了手動拼裝，卻發現配色維護（每次改色手抄多個檔案）吃掉大量時間——該加 templating 工具，或重新考慮整合式。</li>
<li>記憶體壓力浮現（整合式的 Qt runtime 在小機器上排擠其他程式）——往手動拼裝退。</li>
<li>整合式的一次更新靜默破壞了你的自訂設定（<a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia README 明言 config 可能無預警變動</a>）——評估這層快速移動的依賴值不值得。</li>
</ul>
<h2 id="下一步">下一步</h2>
<ul>
<li>整合式 shell 的概念定位、跟 AGS/Eww 的比較、三個 repo 的分工，見 <a href="/blog/linux/dotfile/06-rice-design/caelestia-overview/" data-link-title="Caelestia 總覽：預組裝的 Hyprland 桌面 Shell" data-link-desc="考慮用 Caelestia 取代手動拼裝 waybar&#43;wofi&#43;mako、或評估預組裝桌面 shell 的 trade-off 時回來讀">Caelestia 總覽</a>。</li>
<li>手動拼裝那幾個元件（狀態列、啟動器、通知）各自怎麼配置，見 <a href="/blog/linux/dotfile/06-rice-design/desktop-shell-components/" data-link-title="桌面 Shell 元件：狀態列、啟動器與通知" data-link-desc="Hyprland 桌面要拼哪些元件、各元件的配置檔怎麼寫時回來讀">桌面 Shell 元件</a>。</li>
<li>配色系統本身（不管哪條路線）怎麼設計，見 <a href="/blog/linux/dotfile/06-rice-design/color-system-theming/" data-link-title="配色系統、鎖屏與 GTK 主題" data-link-desc="桌面配色散亂看起來雜、或要換主題不知道該改哪些檔案時回來讀">配色系統、鎖屏與 GTK 主題</a>。</li>
</ul>
<p>這篇的足跡數字（安裝 230 MB vs 4.5 MB、RSS ~400 MB vs ~60 MB）與 lock-died 失敗案例，來自一次在 Apple Silicon UTM VM 上實際跑過兩種桌面棧的量測。</p>
]]></content:encoded></item><item><title>Aurora PG/MySQL vs Aurora DSQL 取捨：何時 single-region managed 夠用、何時跨到 distributed</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/aurora-vs-dsql-tradeoff/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/aurora-vs-dsql-tradeoff/</guid><description>&lt;blockquote>
&lt;p>本文是 Aurora family 內的決策取捨文章。聚焦 &lt;em>standard Aurora（Aurora PostgreSQL / MySQL，single-region managed SQL）&lt;/em> 跟 &lt;em>Aurora DSQL（active-active distributed SQL）&lt;/em> 之間的升級門檻判斷。兩個既有 SSoT 不在本篇重複：「PG → DSQL 怎麼遷」見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &amp;#43; snapshot isolation &amp;#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &amp;#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql&lt;/a>；「DSQL vs Spanner vs CockroachDB 三方 distributed SQL 選型」見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &amp;#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree&lt;/a>。本篇只回答「standard Aurora 夠不夠、要不要跨過去」。&lt;/p>&lt;/blockquote>
&lt;p>多數團隊不需要 Aurora DSQL。Aurora PostgreSQL / MySQL 已經是 managed SQL、storage / compute 分離、跨 AZ 高可用、read replica 擴讀——絕大多數 OLTP workload 在這層就解決了。Aurora DSQL 是 2024-12 re:Invent preview、2025-05 GA 的 &lt;em>不同 paradigm&lt;/em> 產品：PG wire-compatible 但底層是 active-active distributed、OCC + snapshot isolation、multi-region strong consistency。它解的是 standard Aurora &lt;em>解不了&lt;/em> 的特定問題，代價是放棄一部分 PostgreSQL 相容性與交易自由度。要不要跨過去，看 workload 是否真的撞到 standard Aurora 的結構上限。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 Aurora family 內的決策取捨文章。聚焦 <em>standard Aurora（Aurora PostgreSQL / MySQL，single-region managed SQL）</em> 跟 <em>Aurora DSQL（active-active distributed SQL）</em> 之間的升級門檻判斷。兩個既有 SSoT 不在本篇重複：「PG → DSQL 怎麼遷」見 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql</a>；「DSQL vs Spanner vs CockroachDB 三方 distributed SQL 選型」見 <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree</a>。本篇只回答「standard Aurora 夠不夠、要不要跨過去」。</p></blockquote>
<p>多數團隊不需要 Aurora DSQL。Aurora PostgreSQL / MySQL 已經是 managed SQL、storage / compute 分離、跨 AZ 高可用、read replica 擴讀——絕大多數 OLTP workload 在這層就解決了。Aurora DSQL 是 2024-12 re:Invent preview、2025-05 GA 的 <em>不同 paradigm</em> 產品：PG wire-compatible 但底層是 active-active distributed、OCC + snapshot isolation、multi-region strong consistency。它解的是 standard Aurora <em>解不了</em> 的特定問題，代價是放棄一部分 PostgreSQL 相容性與交易自由度。要不要跨過去，看 workload 是否真的撞到 standard Aurora 的結構上限。</p>
<blockquote>
<p><strong>時間錨點</strong>：Aurora DSQL 2024-12 preview、2025-05 GA。vendor 能力持續演進、實際決策前以 AWS docs 當前狀態為準。</p></blockquote>
<h2 id="核心差異single-writer-vs-active-active">核心差異：single-writer vs active-active</h2>
<p>兩者的根本差異在寫入架構：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Aurora PG / MySQL（standard）</th>
          <th>Aurora DSQL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫入架構</td>
          <td>single writer（一個 region 一個 writer）</td>
          <td>active-active（多 region 同時可寫）</td>
      </tr>
      <tr>
          <td>一致性</td>
          <td>單 region 強一致、跨 region 非同步</td>
          <td>multi-region strong consistency</td>
      </tr>
      <tr>
          <td>SQL 相容</td>
          <td>完整 PostgreSQL / MySQL</td>
          <td>PG wire-compatible <em>子集</em>、無多數 extension</td>
      </tr>
      <tr>
          <td>交易模型</td>
          <td>標準 PG/MySQL transaction、長交易</td>
          <td>OCC + snapshot isolation、需處理 retry</td>
      </tr>
      <tr>
          <td>寫入擴展</td>
          <td>受 single writer instance 上限約束</td>
          <td>水平擴展、無 single writer 瓶頸</td>
      </tr>
      <tr>
          <td>運維</td>
          <td>managed、但仍要管 instance / failover</td>
          <td>serverless、zero-touch、無 instance 概念</td>
      </tr>
  </tbody>
</table>
<p>standard Aurora 的 storage 層雖然分散，<em>compute 寫入仍是 single writer</em>——這是它的結構上限。DSQL 把寫入也分散，代價是 SQL 相容性縮窄（PG 子集、extension 缺位）與交易語意改變（OCC，衝突要 application retry）。</p>
<h2 id="該跨到-dsql-的訊號">該跨到 DSQL 的訊號</h2>
<p>只有撞到 standard Aurora 結構上限的特定需求，才值得跨 paradigm：</p>
<ul>
<li><strong>global write（多 region 都要低延遲寫入）</strong>：standard Aurora 跨 region 只有非同步副本、寫入要回到單一 writer region；真正需要多 region active-active 寫入 → DSQL</li>
<li><strong>single-writer 寫入上限撞牆</strong>：寫入量大到單一 writer instance（即使最大 instance class）撐不住、且無法用 sharding 簡單解 → DSQL 的水平寫入擴展</li>
<li><strong>region resiliency（單 region 失效仍要可寫）</strong>：standard Aurora 的跨 region failover 有 RPO/RTO 與寫入中斷；要求單 region 失效時其他 region 仍持續接受寫入 → DSQL active-active</li>
<li><strong>operational zero-touch</strong>：不想管 instance / failover / 容量 → DSQL serverless 模型（但這單項不足以跨 paradigm、要搭配上面的結構需求）</li>
</ul>
<h2 id="不該跨的訊號standard-aurora-夠用">不該跨的訊號（standard Aurora 夠用）</h2>
<p>以下情況跨 DSQL 是過度工程、且會付出相容性代價：</p>
<ul>
<li><strong>single-region 夠用</strong>：寫入集中在一個 region、跨 region 只需要讀副本或 DR → standard Aurora</li>
<li><strong>需要 PostgreSQL extension</strong>：依賴 PostGIS / pgvector / 特定 extension → DSQL 子集不支援、留 standard Aurora</li>
<li><strong>複雜 / 長交易</strong>：依賴長交易、複雜多語句交易、特定 isolation 行為 → standard Aurora 的完整交易模型</li>
<li><strong>寫入量 standard Aurora 撐得住</strong>：single writer 還有餘量 → 不必為「未來可能」預先跨 paradigm</li>
</ul>
<p><code>9.C14 Standard Chartered</code> 與 <code>9.C4 DraftKings</code> 是反向佐證：金融帳本 / 博彩這類高一致性、高關鍵 OLTP workload，在 <em>standard Aurora</em> 上就能同時拿到韌性與性能（DraftKings replication lag 降到 10-30ms 級、Standard Chartered 把韌性與性能當單一目標）。它們沒有跨到 distributed SQL——因為 single-region 強一致 + 跨 AZ 高可用已滿足需求。多數金融 OLTP 不需要 active-active multi-region write。</p>
<blockquote>
<p><strong>Scope warning</strong>：Standard Chartered / DraftKings 的 case 揭露其用 standard Aurora 達成韌性 + 性能（見 <a href="/blog/backend/01-database/vendors/aurora/storage-architecture/" data-link-title="Aurora Storage Architecture：quorum-based 分散式 log 與韌性即性能設計" data-link-desc="Aurora storage / compute 分離、6-way 跨 AZ replication、4-of-6 write / 3-of-6 read quorum、韌性投資自動 amortize 成 read 性能、DraftKings 6ms 寫 / &lt;1ms 讀 production reference">storage-architecture</a>）；「它們不需要 DSQL」是本文基於其 single-region 強一致需求的推論、非 case 明文比較 DSQL。引用為「standard Aurora 已足夠多數高一致 OLTP」的訊號、不當 DSQL 對比的 case fact。</p></blockquote>
<h2 id="升級門檻決策流程">升級門檻決策流程</h2>
<p>從需求判讀到路徑選擇的流程：</p>
<h4 id="step-1確認是不是-global-write-需求">Step 1：確認是不是 global write 需求</h4>
<p>寫入是否真的需要多 region 同時低延遲？還是只需要多 region 讀 + 單 region 寫？後者 standard Aurora（+ Global Database 讀副本）就解。</p>
<h4 id="step-2確認-single-writer-是否真的撞牆">Step 2：確認 single-writer 是否真的撞牆</h4>
<p>當前寫入量 vs 最大 instance class 上限、是否已嘗試過 read/write 分離、是否能用 application 層 sharding。撞牆才考慮 DSQL；沒撞牆是過早優化。</p>
<h4 id="step-3檢查相容性代價">Step 3：檢查相容性代價</h4>
<p>清點對 PG extension、長交易、特定 SQL 功能的依賴。依賴重 → DSQL 相容性子集會擋路、留 standard Aurora。</p>
<h4 id="step-4若決定跨走既有-ssot">Step 4：若決定跨，走既有 SSoT</h4>
<ul>
<li>「PG → DSQL 怎麼遷」（protocol drop-in + paradigm shift、transaction retry 處理、extension 缺位）→ <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql</a></li>
<li>「DSQL vs Spanner vs CockroachDB 哪個 distributed SQL」→ <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree</a></li>
</ul>
<p><strong>Rollback boundary</strong>：跨 paradigm 是高成本決策——DSQL 子集相容性與 OCC 交易模型改變了 application 契約，回退到 standard Aurora 不是改 connection string 就好。決策前用一個非關鍵 workload 試點、確認相容性與 retry 行為，再擴大。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="為什麼這是升級門檻而非遷移">為什麼這是「升級門檻」而非「遷移」</h3>
<p>standard Aurora → DSQL 不是版本升級、是 paradigm 切換。Aurora PG/MySQL 用得好好的，不代表「升級到 DSQL 會更好」——多數情況會更差（失去 extension、交易要改、相容性縮窄）。只有 workload 真的需要 active-active multi-region write 或撞到 single-writer 上限，跨過去才划算。這跟「PostgreSQL major version upgrade」（同 paradigm、向後相容）是完全不同性質的決策。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/aurora/storage-architecture/" data-link-title="Aurora Storage Architecture：quorum-based 分散式 log 與韌性即性能設計" data-link-desc="Aurora storage / compute 分離、6-way 跨 AZ replication、4-of-6 write / 3-of-6 read quorum、韌性投資自動 amortize 成 read 性能、DraftKings 6ms 寫 / &lt;1ms 讀 production reference">storage-architecture</a> — standard Aurora 的 storage 分散但 compute single-writer 的結構上限根源</li>
<li><a href="/blog/backend/01-database/vendors/aurora/global-database-multi-region/" data-link-title="Aurora Global Database：跨 region async replication、&lt; 1 秒 lag 與合規 anti-recommendation" data-link-desc="Aurora Global Database 跨 region storage-level async replication、&lt; 1 秒 typical lag、planned vs unplanned failover RTO 數量級對比、Standard Chartered 合規禁止跨境複製為什麼讓 Global Database 變反指標">global-database-multi-region</a> — standard Aurora 的多 region 方案（非同步副本）、global write 需求前先確認這層夠不夠</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora-dsql/" data-link-title="PostgreSQL → Aurora DSQL Migration：PG wire-compatible Distributed SQL 的 Paradigm Shift" data-link-desc="Aurora DSQL（2024-12 re:Invent preview / 2025-05 GA）是 AWS 推的 PG wire-compatible *active-active distributed SQL*、跟 self-managed PG / Aurora PG 不同 paradigm（OCC &#43; snapshot isolation &#43; multi-region strong consistency）。Migration 結構是 *protocol drop-in &#43; paradigm shift*：app SQL 不太改、但 transaction retry / extension 缺位 / 多 region 一致性需重設計。本文走 DSQL vs Aurora PG vs self-managed PG 三軸對比、為什麼遷的三條 driver（global write / operational zero-touch / region resiliency）、Type E phased plan、5 production 踩雷（transaction retry 沒處理 / extension 缺位 / sequence throughput 限制 / Aurora PG 直升 DSQL 不可行 / region failover semantic）、跟 PG → Aurora 跟 PG → CockroachDB 對比">migrate-to-aurora-dsql</a> — 決定跨之後的遷移 playbook（SSoT）</li>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">aurora-dsql-spanner-decision-tree</a> — 三方 distributed SQL 選型（SSoT）</li>
<li>替代路由：single-region 夠 → 留 standard Aurora；KV access pattern → <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB</a></li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">Standard Chartered 9.C14</a> / <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">DraftKings 9.C4</a> 互引：高一致 OLTP 在 standard Aurora 已足夠的訊號</li>
</ul>
]]></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>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></channel></rss>