<?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>Methodology on Tarragon</title><link>https://tarrragon.github.io/blog/tags/methodology/</link><description>Recent content in Methodology on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/methodology/index.xml" rel="self" type="application/rss+xml"/><item><title>4.13 Eval 設計座標系：三軸、八象限、何時測什麼</title><link>https://tarrragon.github.io/blog/llm/04-applications/eval-design-framework/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/eval-design-framework/</guid><description>&lt;p>LLM 應用的「怎麼測」問題大家都在問、但答案常常是「跑某個 benchmark」「找個 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/llm-as-judge/" data-link-title="LLM-as-Judge" data-link-desc="用 LLM 評估另一個 LLM 的輸出品質、production eval 的主流方法、500-5000× 成本降但有 bias 要處理">LLM judge&lt;/a>」這類&lt;strong>工具層&lt;/strong>回答。實務上工具是末端、設計重點是&lt;strong>先選測什麼軸、再選工具&lt;/strong>。軸選錯了、再好的工具也測不出有用訊號——用 subjective 工具測 objective 行為（例如用 LLM judge 看金額計算對不對）、或用 end-to-end 工具測 component bug（例如看 user satisfaction 但其實是 retrieval pipeline 在漏 chunk）、都是常見的軸誤選。&lt;/p>
&lt;p>本章寫 eval 設計的座標系：三個 binary 軸、八個象限、每個象限對應什麼工具、軸選錯的訊號怎麼識別。這層 framing 是 meta、不是具體 eval 方法——具體方法在 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 benchmarking&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-Judge&lt;/a>。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後你能：&lt;/p>
&lt;ol>
&lt;li>把任何 eval 需求放到三軸座標、定位象限。&lt;/li>
&lt;li>對每個象限選對應的 eval 工具。&lt;/li>
&lt;li>識別軸誤選的訊號、避免「工具對、軸錯」的常見坑。&lt;/li>
&lt;li>規劃 eval 路線：初期該做哪幾個象限、規模化後再補哪些。&lt;/li>
&lt;li>把 eval 設計跟 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 benchmarking&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 tracing&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-Judge&lt;/a> 串成完整 pipeline。&lt;/li>
&lt;/ol>
&lt;h2 id="三軸">三軸&lt;/h2>
&lt;p>Eval 設計的三個正交軸：&lt;/p>
&lt;h3 id="軸-1objective--subjective">軸 1：Objective ↔ Subjective&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Objective&lt;/strong>：有明確 ground truth、檢驗可以寫成 deterministic check（金額對不對、SQL 跑得通不通、JSON schema 合不合法）。&lt;/li>
&lt;li>&lt;strong>Subjective&lt;/strong>：沒有單一正確答案、需要評分或比較（語氣好不好、解釋清楚不清楚、推薦的 trip 合不合用戶）。&lt;/li>
&lt;/ul>
&lt;p>判讀訊號：「能不能用 Python 函數判定對錯」、能 → objective、不能 → subjective。&lt;/p>
&lt;h3 id="軸-2component--end-to-end">軸 2：Component ↔ End-to-End&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Component&lt;/strong>：測單一元件、孤立評估（retrieval 拿對 chunk 沒、tool call 參數對沒、prompt 抽出正確 entity 沒）。&lt;/li>
&lt;li>&lt;strong>End-to-End&lt;/strong>：測完整流程、user 視角結果（user 問題有沒有被解決、訂單有沒有完成、conversation 滿意度）。&lt;/li>
&lt;/ul>
&lt;p>判讀訊號：「失敗時你想知道是哪一段壞掉」→ component；「你只在乎最終體驗」→ end-to-end。&lt;/p>
&lt;h3 id="軸-3quantitative--qualitative">軸 3：Quantitative ↔ Qualitative&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Quantitative&lt;/strong>：產出數字（accuracy / latency / cost / pass rate）、可以追蹤、可以比較、可以 alert。&lt;/li>
&lt;li>&lt;strong>Qualitative&lt;/strong>：產出觀察（error pattern、user 抱怨、reviewer 註記）、無法直接 aggregate、但能引導 hypothesis。&lt;/li>
&lt;/ul>
&lt;p>判讀訊號：「結果能算平均嗎」→ quantitative；「結果是讀完才知道」→ qualitative。&lt;/p></description><content:encoded><![CDATA[<p>LLM 應用的「怎麼測」問題大家都在問、但答案常常是「跑某個 benchmark」「找個 <a href="/blog/llm/knowledge-cards/llm-as-judge/" data-link-title="LLM-as-Judge" data-link-desc="用 LLM 評估另一個 LLM 的輸出品質、production eval 的主流方法、500-5000× 成本降但有 bias 要處理">LLM judge</a>」這類<strong>工具層</strong>回答。實務上工具是末端、設計重點是<strong>先選測什麼軸、再選工具</strong>。軸選錯了、再好的工具也測不出有用訊號——用 subjective 工具測 objective 行為（例如用 LLM judge 看金額計算對不對）、或用 end-to-end 工具測 component bug（例如看 user satisfaction 但其實是 retrieval pipeline 在漏 chunk）、都是常見的軸誤選。</p>
<p>本章寫 eval 設計的座標系：三個 binary 軸、八個象限、每個象限對應什麼工具、軸選錯的訊號怎麼識別。這層 framing 是 meta、不是具體 eval 方法——具體方法在 <a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 benchmarking</a> 跟 <a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-Judge</a>。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後你能：</p>
<ol>
<li>把任何 eval 需求放到三軸座標、定位象限。</li>
<li>對每個象限選對應的 eval 工具。</li>
<li>識別軸誤選的訊號、避免「工具對、軸錯」的常見坑。</li>
<li>規劃 eval 路線：初期該做哪幾個象限、規模化後再補哪些。</li>
<li>把 eval 設計跟 <a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 benchmarking</a> / <a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 tracing</a> / <a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-Judge</a> 串成完整 pipeline。</li>
</ol>
<h2 id="三軸">三軸</h2>
<p>Eval 設計的三個正交軸：</p>
<h3 id="軸-1objective--subjective">軸 1：Objective ↔ Subjective</h3>
<ul>
<li><strong>Objective</strong>：有明確 ground truth、檢驗可以寫成 deterministic check（金額對不對、SQL 跑得通不通、JSON schema 合不合法）。</li>
<li><strong>Subjective</strong>：沒有單一正確答案、需要評分或比較（語氣好不好、解釋清楚不清楚、推薦的 trip 合不合用戶）。</li>
</ul>
<p>判讀訊號：「能不能用 Python 函數判定對錯」、能 → objective、不能 → subjective。</p>
<h3 id="軸-2component--end-to-end">軸 2：Component ↔ End-to-End</h3>
<ul>
<li><strong>Component</strong>：測單一元件、孤立評估（retrieval 拿對 chunk 沒、tool call 參數對沒、prompt 抽出正確 entity 沒）。</li>
<li><strong>End-to-End</strong>：測完整流程、user 視角結果（user 問題有沒有被解決、訂單有沒有完成、conversation 滿意度）。</li>
</ul>
<p>判讀訊號：「失敗時你想知道是哪一段壞掉」→ component；「你只在乎最終體驗」→ end-to-end。</p>
<h3 id="軸-3quantitative--qualitative">軸 3：Quantitative ↔ Qualitative</h3>
<ul>
<li><strong>Quantitative</strong>：產出數字（accuracy / latency / cost / pass rate）、可以追蹤、可以比較、可以 alert。</li>
<li><strong>Qualitative</strong>：產出觀察（error pattern、user 抱怨、reviewer 註記）、無法直接 aggregate、但能引導 hypothesis。</li>
</ul>
<p>判讀訊號：「結果能算平均嗎」→ quantitative；「結果是讀完才知道」→ qualitative。</p>
<h3 id="三軸的正交性">三軸的正交性</h3>
<p>這三軸是正交的、不是同義詞：</p>
<ul>
<li>「Objective + component + quantitative」典型是 unit test（function 返回對不對）。</li>
<li>「Subjective + end-to-end + qualitative」典型是 user 訪談（user 整體滿意度）。</li>
<li>中間象限存在多種混合、各有對應工具。</li>
</ul>
<h2 id="八象限">八象限</h2>
<p>3 個 binary 軸 = 8 象限。每個象限的常見對應工具：</p>
<table>
  <thead>
      <tr>
          <th>象限</th>
          <th>典型問題</th>
          <th>對應工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Objective + Component + Quantitative</td>
          <td>這個函數 / tool / RAG 元件對嗎</td>
          <td>Unit test、deterministic check、<a href="/blog/llm/knowledge-cards/retrieval-recall/" data-link-title="Retrieval Recall" data-link-desc="衡量 RAG 檢索是否把應該命中的文件或 chunk 放進 top-k 結果，是 component-level eval 的核心指標">retrieval recall@k</a></td>
      </tr>
      <tr>
          <td>Objective + Component + Qualitative</td>
          <td>這個元件失敗 pattern 是什麼</td>
          <td>Error log 分析、trace inspection</td>
      </tr>
      <tr>
          <td>Objective + End-to-end + Quantitative</td>
          <td>整套系統的 success rate / latency</td>
          <td>E2E test、success metric、latency p95</td>
      </tr>
      <tr>
          <td>Objective + End-to-end + Qualitative</td>
          <td>整套系統的 catastrophic 失敗 case 是什麼</td>
          <td>Production incident review、抽樣 trace 讀</td>
      </tr>
      <tr>
          <td>Subjective + Component + Quantitative</td>
          <td>這個 step 的輸出評分</td>
          <td>LLM-as-judge pairwise / rubric、human rating</td>
      </tr>
      <tr>
          <td>Subjective + Component + Qualitative</td>
          <td>這個 step 的 output 哪裡讓人不舒服</td>
          <td>Human review、error analysis with comments</td>
      </tr>
      <tr>
          <td>Subjective + End-to-end + Quantitative</td>
          <td>User 整體 NPS / 滿意度評分</td>
          <td>CSAT、thumbs up/down、appeal rate</td>
      </tr>
      <tr>
          <td>Subjective + End-to-end + Qualitative</td>
          <td>User 想要的是什麼、現在哪裡沒滿足</td>
          <td>User 訪談、開放問卷、social listening</td>
      </tr>
  </tbody>
</table>
<p>不是「八個都要做」、是「先看你的問題在哪個象限、用對應工具」。</p>
<p>兩個最容易誤判的象限展開：</p>
<p><strong>Subjective + Component + Quantitative</strong>（這個 step 輸出評分）：對應工具列「LLM-as-judge pairwise / rubric、human rating」、但 <strong>pairwise 是首選、不是 rubric</strong>——pairwise 比較讓 judge 的偏差更可控（兩個答案放在一起比、誰好誰差比較好判）、rubric 容易受 verbosity / position bias 影響。Rubric 留給「需要絕對分數而非相對排序」的場景（如要追蹤絕對品質漂移）。詳見 <a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-Judge</a> 的 bias 緩解段。</p>
<p><strong>Objective + Component + Quantitative</strong>（元件對嗎）：這象限最容易做、cost 也最低——deterministic check 配 component test、CI 跑、production trace 隨抽即驗。Production AI 系統若這象限沒覆蓋、bug 永遠靠 user 抱怨才發現、debug 跟 incident review 成本高。對應反例：把這象限的測試交給 LLM judge（見軸誤選一）。</p>
<h2 id="軸誤選的訊號">軸誤選的訊號</h2>
<p>軸選錯時、工具會給出「看起來合理但其實沒用」的訊號。三個常見軸誤選：</p>
<h3 id="誤選一用-subjective-工具測-objective-行為">誤選一：用 subjective 工具測 objective 行為</h3>
<p>例：訂單金額計算對不對、找 LLM judge 來看「這個金額合理嗎」。</p>
<ul>
<li><strong>問題</strong>：金額計算有 ground truth、應該 deterministic check（<code>assert order.total == expected</code>）。LLM judge 對「合理」的判斷有偏差、會放過明顯錯誤、會挑剔正確但不直觀的答案。</li>
<li><strong>訊號</strong>：你發現自己在寫「judge prompt」描述「什麼樣的金額是合理的」、但其實該行為有客觀標準。</li>
<li><strong>修正</strong>：把 judge prompt 翻成 deterministic check。</li>
</ul>
<h3 id="誤選二用-end-to-end-工具測-component-bug">誤選二：用 end-to-end 工具測 component bug</h3>
<p>例：整套系統 success rate 從 90% 掉到 80%、追了一週、結果是 retrieval 漏 chunk。</p>
<ul>
<li><strong>問題</strong>：E2E metric 告訴你「有問題」、不告訴你「在哪」。Component eval 缺失時、debug 從 trace 倒推、耗時。</li>
<li><strong>訊號</strong>：incident 後 root cause analysis 經常超過一天、查到的東西其實 component eval 該秒抓。</li>
<li><strong>修正</strong>：對 critical component（retrieval、tool 調用、parse 階段）加 component eval、production 持續跑。</li>
</ul>
<h3 id="誤選三用-quantitative-工具找-qualitative-訊號">誤選三：用 quantitative 工具找 qualitative 訊號</h3>
<p>例：user 滿意度從 4.2 掉到 4.0、團隊看數字盯一週、不知道發生什麼。</p>
<ul>
<li><strong>問題</strong>：Quantitative metric 只告訴你「有變化」、不告訴你「為什麼」。Qualitative 訊號（user 抱怨內容、抽樣 conversation）才能浮現 hypothesis。</li>
<li><strong>訊號</strong>：團隊看 dashboard 看了很久、卻沒人去讀 actual user feedback。</li>
<li><strong>修正</strong>：quantitative trigger（指標漂移）、qualitative 跟進（讀樣本、找 pattern）。</li>
</ul>
<h2 id="eval-演化路徑">Eval 演化路徑</h2>
<p>不同階段的 LLM 應用、該優先補哪些象限不同。</p>
<h3 id="階段-0mvp沒任何-eval">階段 0：MVP（沒任何 eval）</h3>
<p>問題：「能不能 demo 一下就好」、行為對不對全靠手測。</p>
<ul>
<li><strong>第一個該補的</strong>：Objective + End-to-end + Quantitative。最少跑 10 個 representative case、能看「跑得起來率」就好。</li>
<li><strong>不該太早做</strong>：subjective eval、需要 judge / human rating 的東西。MVP 階段先讓系統穩定運行。</li>
</ul>
<h3 id="階段-1有-user-在用">階段 1：有 user 在用</h3>
<p>問題：production 偶爾有 bug、user 偶爾抱怨、不知道哪些是 systematic、哪些是 random。</p>
<ul>
<li><strong>第二個該補的</strong>：Objective + End-to-end + Qualitative。讀 incident、讀抽樣 trace、找 pattern。</li>
<li><strong>第三個該補的</strong>：Objective + Component + Quantitative。對 critical component（retrieval / tool call / parse）加 component-level eval、production 跑。</li>
<li><strong>不該做</strong>：完整 subjective rubric。先把 objective 失敗修了再說。</li>
</ul>
<h3 id="階段-2要持續優化品質">階段 2：要持續優化品質</h3>
<p>問題：objective 部分已經穩、user 抱怨主要在 subjective 層（語氣、helpful 程度、推薦合不合用）。</p>
<ul>
<li><strong>第四個該補的</strong>：Subjective + Component + Quantitative。用 LLM-as-judge 給每個 step 評分、做 A/B test 比較 prompt 變動。</li>
<li><strong>第五個該補的</strong>：Subjective + End-to-end + Quantitative。CSAT、thumbs up/down、appeal rate。</li>
<li><strong>要做的</strong>：Subjective eval 跟 qualitative review 必須配合進行——quantitative 給出方向、qualitative 給出修法 hypothesis。</li>
</ul>
<h3 id="階段-3規模化跨團隊">階段 3：規模化、跨團隊</h3>
<p>問題：多個產品 / 團隊用同一套 LLM infra、eval 要 cross-cutting。</p>
<ul>
<li><strong>要做的</strong>：標準化 eval pipeline、把象限 1-7 都 cover、qualitative review 進入 ritual（每週 incident review、每月抽樣 trace 讀）。</li>
<li><strong>重點不是「全部都有」、而是「每個象限的 owner 清楚」</strong>。</li>
</ul>
<h2 id="eval-跟-trace-的閉環">Eval 跟 Trace 的閉環</h2>
<p>Eval 不是孤立的——它跟 <a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 LLM tracing</a> 形成閉環：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">[Production traffic]
</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">   [LLM trace]  ← 每次 call / agent step / tool 都記錄
</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">   ├── 即時 monitoring（latency / cost / error rate）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   ├── 抽樣進 eval set（人工標 + LLM judge）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   └── failed case 進 regression set（防止改 prompt 又壞同樣 case）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">       ↓
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   [Eval pipeline]
</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">   ├── Component eval（單元件 accuracy）
</span></span><span class="line"><span class="ln">12</span><span class="cl">   ├── E2E eval（整套 success rate）
</span></span><span class="line"><span class="ln">13</span><span class="cl">   └── Subjective eval（judge / human rating）
</span></span><span class="line"><span class="ln">14</span><span class="cl">       ↓
</span></span><span class="line"><span class="ln">15</span><span class="cl">   [Insights]
</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">   ├── Quantitative：metric 漂移 alert
</span></span><span class="line"><span class="ln">18</span><span class="cl">   └── Qualitative：error pattern → hypothesis → 修 prompt / tool / RAG
</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">   [改動進 production]
</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">   [回到 production traffic、看 metric 收斂]</span></span></code></pre></div><p>Production trace 不只是 debug 工具、是 eval set 的活泉。Trace + eval 閉環的設計細節見 <a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20</a>。</p>
<h2 id="跟其他-eval-章節的分工">跟其他 Eval 章節的分工</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>焦點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/04-applications/eval-design-framework/" data-link-title="4.13 Eval 設計座標系：三軸、八象限、何時測什麼" data-link-desc="Eval 設計三軸（objective↔subjective / component↔end-to-end / quantitative↔qualitative）、八象限的對應 eval 工具、軸選錯的訊號、跟 benchmarking / LLM-as-judge / tracing 的關係">4.13 本章</a></td>
          <td><strong>Meta</strong>：先選軸、再選工具的設計座標系</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 Benchmarking</a></td>
          <td>具體 benchmark 跟自家 eval set 的方法論</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 LLM tracing</a></td>
          <td>Trace 怎麼接 eval、production observability</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-Judge</a></td>
          <td>Subjective eval 的核心工具、rubric / pairwise / bias 緩解</td>
      </tr>
  </tbody>
</table>
<p>讀法建議：先讀本章建立座標系、再依當前痛點往對應章節展開。Subjective eval 痛點 → 4.21；自家 benchmark 設計 → 4.14；production observability → 4.20。</p>
<h2 id="有效-eval-系統的四個設計條件">有效 eval 系統的四個設計條件</h2>
<p>Eval 系統要持續產生有用訊號、必須滿足四個條件。每個條件對應一個常見退化模式、可同時當 checklist 用。</p>
<h3 id="條件一judge-只用在-subjective-軸">條件一：Judge 只用在 subjective 軸</h3>
<p>LLM-as-judge 留給沒 ground truth 的 subjective 行為（語氣、helpful 程度、解釋清楚）、objective 行為（金額、JSON schema、API 參數）用 deterministic check。Judge 的 cost 比 deterministic check 高 1-2 個數量級、精度反而不如、明顯不划算。</p>
<p>對應反例：「全部 eval 都做成 LLM judge」——judge 被誤用在 objective 行為、cost 翻倍、精度反降。</p>
<h3 id="條件二每個-metric-有-ownerthresholdaction">條件二：每個 metric 有 owner、threshold、action</h3>
<p>每個 production metric 都要明確：誰負責看（owner）、什麼數字觸發 alert（threshold）、alert 後做什麼（action）。沒這三項的 metric 是 noise。</p>
<p>對應反例：dashboard 上 50 個 metric 圖、沒人定期看、bug 還是靠 user 抱怨才知道。</p>
<h3 id="條件三eval-set-跟-production-traffic-同步">條件三：Eval set 跟 production traffic 同步</h3>
<p>Production trace 持續抽樣補進 eval set、每季 review eval set 跟 traffic 分佈是否一致。</p>
<p>對應反例：eval set 是兩年前定的、production traffic 已經漂得很遠、eval 通過不代表 user 滿意。</p>
<h3 id="條件四保留-frozen-baseline">條件四：保留 frozen baseline</h3>
<p><a href="/blog/llm/knowledge-cards/frozen-baseline/" data-link-title="Frozen baseline" data-link-desc="Eval 系統中固定特定 prompt &#43; model 當長期對照、讓行為漂移可見的標準作法">Frozen baseline</a> 是把某個特定 prompt + 特定 model 跑 production 一段時間後 freeze 起來、每次新版本跟它比、定期 refresh 並標明時點。漂移看得見才能管理。</p>
<p>對應反例：每次 A/B 都跟「最新版本」比、長期累積漂移完全不可見、「整體變好了沒」無從回答。</p>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>三軸座標（objective / component / quantitative 三個 binary 軸）。</li>
<li>八象限對應工具的結構分類。</li>
<li>三類軸誤選的識別訊號跟修正。</li>
<li>Eval 演化路徑（MVP → user → 優化 → 規模化）。</li>
<li>Eval / trace 閉環的設計。</li>
<li>有效 eval 系統的四個設計條件。</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體 eval framework（OpenAI Evals、Promptfoo、Braintrust、Langfuse 等會持續演化）。</li>
<li>LLM-as-judge 的具體 prompt 模板跟 bias 緩解技巧。</li>
<li>各 benchmark 的權威性（半年一換）。</li>
</ul>
<h2 id="下一章">下一章</h2>
<p>下一章：<a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 Benchmarking 與評估方法論</a>、把座標系落到具體 benchmark 設計。Subjective eval 的工具見 <a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-Judge</a>、production trace 怎麼接 eval 見 <a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 LLM tracing</a>、跟 fuzzy engineering 典範的關係見 <a href="/blog/llm/00-foundations/deterministic-vs-fuzzy-engineering/" data-link-title="0.8 Deterministic vs Fuzzy Engineering：軟體設計典範的位移" data-link-desc="傳統 deterministic 軟體跟 fuzzy LLM 軟體在資料、邏輯、分解、實驗成本四個維度的根本差異、以及哪段該 deterministic、哪段該 fuzzy 的決策框架">0.8</a>（fuzzy 行為的測試本質就是 distribution metric）。</p>
]]></content:encoded></item><item><title>Background Agent 平行研究：main context 節省的量化效應</title><link>https://tarrragon.github.io/blog/record/background-agent-%E5%B9%B3%E8%A1%8C%E7%A0%94%E7%A9%B6main-context-%E7%AF%80%E7%9C%81%E7%9A%84%E9%87%8F%E5%8C%96%E6%95%88%E6%87%89/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/background-agent-%E5%B9%B3%E8%A1%8C%E7%A0%94%E7%A9%B6main-context-%E7%AF%80%E7%9C%81%E7%9A%84%E9%87%8F%E5%8C%96%E6%95%88%E6%87%89/</guid><description>&lt;p>跨多個獨立子任務的研究（如多個 vendor 案例採集、多個主題 web research、多個檔案的 fact-check）、用 background agent 平行做、比串行單一 agent 或主 context 直接做都更省 token。&lt;/p>
&lt;p>這份紀錄整理 backend/03-message-queue 模組 6 vendor case 庫採集的實作經驗、量化 main context 節省效應、給未來類似任務作為設定參考。&lt;/p>
&lt;h2 id="採集任務的特徵">採集任務的特徵&lt;/h2>
&lt;p>backend/03 模組需要為 6 個 vendor（Kafka / RabbitMQ / NATS / Redis Streams / SQS / Pub/Sub）採集 5-10 個公開 case。任務特徵：&lt;/p>
&lt;ul>
&lt;li>各 vendor 獨立、無相互依賴&lt;/li>
&lt;li>每個 vendor 需要 WebSearch 找候選 + WebFetch 驗證 URL + 抽 finding、多步驟&lt;/li>
&lt;li>每個 agent 任務時長 4-7 分鐘（含 WebFetch 多次往返）&lt;/li>
&lt;li>採集回報是清單形式、易於主 context 整合&lt;/li>
&lt;/ul>
&lt;h2 id="background-agent-平行的執行方式">Background agent 平行的執行方式&lt;/h2>
&lt;p>每個 agent 用 &lt;code>subagent_type: general-purpose&lt;/code>、&lt;code>run_in_background: true&lt;/code>、&lt;code>prompt&lt;/code> 含：&lt;/p>
&lt;ol>
&lt;li>採集目標（5-10 案例）&lt;/li>
&lt;li>硬閘門（WebFetch 驗證）&lt;/li>
&lt;li>排除清單（已有案例 / vendor 自家 marketing）&lt;/li>
&lt;li>對齊大綱（該 vendor 的進階主題列表）&lt;/li>
&lt;li>回傳格式（清單、含 source / observation / finding / 對應章節）&lt;/li>
&lt;/ol>
&lt;p>主 context 一個 message spawn 6 個 agent、然後等通知。&lt;/p>
&lt;h2 id="量化結果">量化結果&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>串行單 agent&lt;/th>
 &lt;th>Background 平行 6 agent&lt;/th>
 &lt;th>主 context 直接做&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>總時間&lt;/td>
 &lt;td>~40 分鐘（6 vendor × 7 分鐘）&lt;/td>
 &lt;td>~7 分鐘（最慢 agent）&lt;/td>
 &lt;td>~60 分鐘（含探索盲區）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主 context token&lt;/td>
 &lt;td>高（每次 WebFetch 都進 context）&lt;/td>
 &lt;td>低（只收 summary）&lt;/td>
 &lt;td>最高（整個流程在 context）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Agent context token&lt;/td>
 &lt;td>跟串行同&lt;/td>
 &lt;td>每 agent 獨立、不影響主&lt;/td>
 &lt;td>N/A&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗風險&lt;/td>
 &lt;td>任一 agent 失敗影響全部&lt;/td>
 &lt;td>失敗 agent 獨立、其他繼續&lt;/td>
 &lt;td>主 context 失敗整體中斷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>主 context 節省效應 ~80%：每個 agent 報告約 2KB summary、6 個總 12KB；若主 context 直接做、每次 WebFetch 取回的 markdown 約 10-30KB、累積後容易 &amp;gt; 100KB。&lt;/p>
&lt;h2 id="適用場景判斷">適用場景判斷&lt;/h2>
&lt;p>Background agent 平行適用：&lt;/p>
&lt;ul>
&lt;li>多個&lt;strong>獨立子任務&lt;/strong>（不互相依賴 input / output）&lt;/li>
&lt;li>每個子任務需要&lt;strong>多步驟 tool use&lt;/strong>（WebFetch / WebSearch / Bash / Glob）&lt;/li>
&lt;li>子任務回報是&lt;strong>結構化清單 / summary&lt;/strong>、不是 raw transcript&lt;/li>
&lt;li>主 context 需要&lt;strong>節省 token&lt;/strong> 做後續工作（如寫檔、整理 index）&lt;/li>
&lt;/ul>
&lt;p>不適用：&lt;/p></description><content:encoded><![CDATA[<p>跨多個獨立子任務的研究（如多個 vendor 案例採集、多個主題 web research、多個檔案的 fact-check）、用 background agent 平行做、比串行單一 agent 或主 context 直接做都更省 token。</p>
<p>這份紀錄整理 backend/03-message-queue 模組 6 vendor case 庫採集的實作經驗、量化 main context 節省效應、給未來類似任務作為設定參考。</p>
<h2 id="採集任務的特徵">採集任務的特徵</h2>
<p>backend/03 模組需要為 6 個 vendor（Kafka / RabbitMQ / NATS / Redis Streams / SQS / Pub/Sub）採集 5-10 個公開 case。任務特徵：</p>
<ul>
<li>各 vendor 獨立、無相互依賴</li>
<li>每個 vendor 需要 WebSearch 找候選 + WebFetch 驗證 URL + 抽 finding、多步驟</li>
<li>每個 agent 任務時長 4-7 分鐘（含 WebFetch 多次往返）</li>
<li>採集回報是清單形式、易於主 context 整合</li>
</ul>
<h2 id="background-agent-平行的執行方式">Background agent 平行的執行方式</h2>
<p>每個 agent 用 <code>subagent_type: general-purpose</code>、<code>run_in_background: true</code>、<code>prompt</code> 含：</p>
<ol>
<li>採集目標（5-10 案例）</li>
<li>硬閘門（WebFetch 驗證）</li>
<li>排除清單（已有案例 / vendor 自家 marketing）</li>
<li>對齊大綱（該 vendor 的進階主題列表）</li>
<li>回傳格式（清單、含 source / observation / finding / 對應章節）</li>
</ol>
<p>主 context 一個 message spawn 6 個 agent、然後等通知。</p>
<h2 id="量化結果">量化結果</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>串行單 agent</th>
          <th>Background 平行 6 agent</th>
          <th>主 context 直接做</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>總時間</td>
          <td>~40 分鐘（6 vendor × 7 分鐘）</td>
          <td>~7 分鐘（最慢 agent）</td>
          <td>~60 分鐘（含探索盲區）</td>
      </tr>
      <tr>
          <td>主 context token</td>
          <td>高（每次 WebFetch 都進 context）</td>
          <td>低（只收 summary）</td>
          <td>最高（整個流程在 context）</td>
      </tr>
      <tr>
          <td>Agent context token</td>
          <td>跟串行同</td>
          <td>每 agent 獨立、不影響主</td>
          <td>N/A</td>
      </tr>
      <tr>
          <td>失敗風險</td>
          <td>任一 agent 失敗影響全部</td>
          <td>失敗 agent 獨立、其他繼續</td>
          <td>主 context 失敗整體中斷</td>
      </tr>
  </tbody>
</table>
<p>主 context 節省效應 ~80%：每個 agent 報告約 2KB summary、6 個總 12KB；若主 context 直接做、每次 WebFetch 取回的 markdown 約 10-30KB、累積後容易 &gt; 100KB。</p>
<h2 id="適用場景判斷">適用場景判斷</h2>
<p>Background agent 平行適用：</p>
<ul>
<li>多個<strong>獨立子任務</strong>（不互相依賴 input / output）</li>
<li>每個子任務需要<strong>多步驟 tool use</strong>（WebFetch / WebSearch / Bash / Glob）</li>
<li>子任務回報是<strong>結構化清單 / summary</strong>、不是 raw transcript</li>
<li>主 context 需要<strong>節省 token</strong> 做後續工作（如寫檔、整理 index）</li>
</ul>
<p>不適用：</p>
<ul>
<li>線性依賴（任務 B 需要任務 A 結果）</li>
<li>短任務（單一 WebFetch、串行直接做更快、平行 overhead 不划算）</li>
<li>需要主 context 即時介入決策的任務</li>
</ul>
<h2 id="跟其他-agent-用法的對比">跟其他 agent 用法的對比</h2>
<p>backend 模組過去用過的其他 agent 用法：</p>
<table>
  <thead>
      <tr>
          <th>用法</th>
          <th>階段</th>
          <th>目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stage 0 平行採集</td>
          <td>寫作前</td>
          <td>研究、補案例庫</td>
      </tr>
      <tr>
          <td>Stage 3 平行 review</td>
          <td>寫作後</td>
          <td>審查、抓 issue</td>
      </tr>
      <tr>
          <td>即時 Explore agent</td>
          <td>寫作中</td>
          <td>找 file / symbol 位置</td>
      </tr>
  </tbody>
</table>
<p>三種都用 background、都節省主 context、但目的跟回報格式不同。Stage 0 採集回報是「<strong>清單 + 捨棄候選</strong>」、Stage 3 review 回報是「<strong>issue list + severity</strong>」、Explore 回報是「<strong>file path + match</strong>」。</p>
<h2 id="設定參考">設定參考</h2>
<p>spawn 平行 agent 的 anti-pattern：</p>
<ul>
<li><strong>不寫硬閘門</strong>：「找 5-10 case」沒明示 WebFetch 驗證 → agent 編造 URL</li>
<li><strong>不列排除清單</strong>：「找 Kafka 案例」沒列既有案例 → agent 重複採集</li>
<li><strong>要求 raw transcript 回報</strong>：「把找到的內容貼給我」→ 主 context 爆炸</li>
<li><strong>單一巨大 agent</strong>：「找所有 6 個 vendor」串行做 → 失去平行優勢</li>
<li><strong>平行過頭</strong>：spawn 20+ agent 但實際只有 6 個獨立任務 → 不必要的協調成本</li>
</ul>
<h2 id="跟-case-first-流程的關係">跟 case-first 流程的關係</h2>
<p>這個方法已寫入 <code>.claude/skills/case-first-module-workflow/references/stage-0-case-collection.md</code>、成為 case-first 流程的 stage 0 採集標準執行範式。但實際適用範圍超出 case 採集、適用所有「多獨立子任務 + 多步驟 tool use」場景。</p>
<h2 id="下一步該追蹤的議題">下一步該追蹤的議題</h2>
<ol>
<li><strong>平行 agent 數量上限</strong>：6 個跑 OK、20+ 是否會撞到 rate limit 或協調成本？實作上限是多少？</li>
<li><strong>Agent context 跑滿後的恢復策略</strong>：若某個 agent context 跑滿、其他 agent 繼續但該 agent 失敗、要不要 retry？怎麼接續？</li>
<li><strong>跨 agent 共享 cache</strong>：6 個 agent 都 WebSearch 同一個 vendor 主頁、有沒有 cache 共享機制可省 token？目前每 agent 獨立、可能重複 fetch</li>
</ol>
]]></content:encoded></item><item><title>Methodology 的 multi-pass 該升級為 pillar 層：核心結構才會被執行</title><link>https://tarrragon.github.io/blog/report/methodology-multi-pass-embedding/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/methodology-multi-pass-embedding/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>凡是教做事方法的東西（SKILL、playbook、methodology document、checklist）— 如果你認為 multi-pass refinement 是必要的、就要把它放在&lt;strong>核心結構層&lt;/strong>（pillar、principle、step）、不是放在&lt;strong>附帶段&lt;/strong>（appendix、tips、reminder、see also）。&lt;/p>
&lt;p>放在 appendix = 結構暗示「optional、看心情選擇」 = 在 &lt;a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發&lt;/a> 的結構壓力下、永遠被跳過。&lt;strong>Pillar 層 = 結構性必跑、用結構強制行為、不靠紀律&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-pillar--appendix-的位置決定執行率">為什麼 pillar / appendix 的位置決定執行率&lt;/h2>
&lt;p>讀者看 SKILL / methodology 時、認知資源分配：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Pillar / Core Principles&lt;/strong>：必讀、會內化、實作中會回想&lt;/li>
&lt;li>&lt;strong>Steps / Reference&lt;/strong>：實作中翻&lt;/li>
&lt;li>&lt;strong>Tips / Appendix / &amp;ldquo;See also&amp;rdquo;&lt;/strong>：第一次讀掃過、之後忘記&lt;/li>
&lt;/ul>
&lt;p>把 multi-pass review 放 appendix = 結構暗示「這是進階、可選」。即使內容寫得很詳細、結構訊號蓋過內容。&lt;/p>
&lt;p>對比放 pillar：每次接觸 SKILL、第一眼看到 4-5 個 pillar 中包含 &amp;ldquo;Multi-pass Refinement&amp;rdquo; — 結構性提示「這跟其他 pillar 同樣重要」。&lt;/p>
&lt;hr>
&lt;h2 id="各-methodology-的-pillar--appendix-切分">各 methodology 的 pillar / appendix 切分&lt;/h2>
&lt;p>實際 methodology 文件的 pillar 應該包含 multi-pass、appendix 應該避免：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Methodology&lt;/th>
 &lt;th>適合的 pillar&lt;/th>
 &lt;th>不適合放 appendix&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>compositional-writing（寫作方法論）&lt;/td>
 &lt;td>第 6 原則「Re-read Pass」明示輪次&lt;/td>
 &lt;td>「最後 review 一下」三字附帶&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>requirement-protocol（需求協議）&lt;/td>
 &lt;td>第 4 pillar「Multi-pass Refinement」明示「第 1 輪實作預期不對」&lt;/td>
 &lt;td>「失敗多次再回頭看」零散提示&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>frontend-with-playwright（前端 + 測試協議）&lt;/td>
 &lt;td>「漸進驗證」在 6 大原則中（已有）、再加「Multi-pass Review」串成系列&lt;/td>
 &lt;td>TODO 註解講「之後 review」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TDD（test-driven）&lt;/td>
 &lt;td>RED-GREEN-REFACTOR 三步本身就是 multi-pass&lt;/td>
 &lt;td>「重構是 optional」當 appendix&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Agile（process）&lt;/td>
 &lt;td>Sprint review / retrospective 是 pillar&lt;/td>
 &lt;td>「有空回顧一下」當 appendix&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 methodology 的設計都該檢查：&lt;strong>multi-pass 是 pillar 還是 appendix？&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="如何識別該升-pillar-但被當-appendix">如何識別「該升 pillar 但被當 appendix」&lt;/h2>
&lt;p>訊號：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「最後再 review 一下」「有空再 polish」這類 disclaimer&lt;/td>
 &lt;td>升成獨立 pillar / 原則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-pass 內容散在多個 reference 角落、沒有單一定位&lt;/td>
 &lt;td>抽出 pillar、各 reference 引用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pillar 列表只 3 條（看似簡潔）、但實作中常忘 review&lt;/td>
 &lt;td>缺 pillar、補上 multi-pass&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「第 1 輪原則」+「第 2 輪原則」分開兩個 SKILL&lt;/td>
 &lt;td>合併、multi-pass 是同 SKILL 的多輪、不是兩個 SKILL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>文件結尾「最後注意事項」常被使用者引用為「我忘了」&lt;/td>
 &lt;td>結構問題、移到 pillar&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個訊號都是 &lt;strong>multi-pass 的位置太低&lt;/strong>、結構壓力把它當作 optional。&lt;/p>
&lt;hr>
&lt;h2 id="升-pillar-後的設計四個必要元素">升 pillar 後的設計：四個必要元素&lt;/h2>
&lt;p>把 multi-pass 升成 pillar、需要含這四個元素才完整：&lt;/p>
&lt;h3 id="1-明示第-1-輪不追求完美">1. 明示「第 1 輪不追求完美」&lt;/h3>
&lt;p>寫在 pillar 內容、第一句就講：「第 1 輪不要追求 perfect、預期會有未發現問題、設計第 2 輪去 catch」。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>凡是教做事方法的東西（SKILL、playbook、methodology document、checklist）— 如果你認為 multi-pass refinement 是必要的、就要把它放在<strong>核心結構層</strong>（pillar、principle、step）、不是放在<strong>附帶段</strong>（appendix、tips、reminder、see also）。</p>
<p>放在 appendix = 結構暗示「optional、看心情選擇」 = 在 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> 的結構壓力下、永遠被跳過。<strong>Pillar 層 = 結構性必跑、用結構強制行為、不靠紀律</strong>。</p>
<hr>
<h2 id="為什麼-pillar--appendix-的位置決定執行率">為什麼 pillar / appendix 的位置決定執行率</h2>
<p>讀者看 SKILL / methodology 時、認知資源分配：</p>
<ul>
<li><strong>Pillar / Core Principles</strong>：必讀、會內化、實作中會回想</li>
<li><strong>Steps / Reference</strong>：實作中翻</li>
<li><strong>Tips / Appendix / &ldquo;See also&rdquo;</strong>：第一次讀掃過、之後忘記</li>
</ul>
<p>把 multi-pass review 放 appendix = 結構暗示「這是進階、可選」。即使內容寫得很詳細、結構訊號蓋過內容。</p>
<p>對比放 pillar：每次接觸 SKILL、第一眼看到 4-5 個 pillar 中包含 &ldquo;Multi-pass Refinement&rdquo; — 結構性提示「這跟其他 pillar 同樣重要」。</p>
<hr>
<h2 id="各-methodology-的-pillar--appendix-切分">各 methodology 的 pillar / appendix 切分</h2>
<p>實際 methodology 文件的 pillar 應該包含 multi-pass、appendix 應該避免：</p>
<table>
  <thead>
      <tr>
          <th>Methodology</th>
          <th>適合的 pillar</th>
          <th>不適合放 appendix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>compositional-writing（寫作方法論）</td>
          <td>第 6 原則「Re-read Pass」明示輪次</td>
          <td>「最後 review 一下」三字附帶</td>
      </tr>
      <tr>
          <td>requirement-protocol（需求協議）</td>
          <td>第 4 pillar「Multi-pass Refinement」明示「第 1 輪實作預期不對」</td>
          <td>「失敗多次再回頭看」零散提示</td>
      </tr>
      <tr>
          <td>frontend-with-playwright（前端 + 測試協議）</td>
          <td>「漸進驗證」在 6 大原則中（已有）、再加「Multi-pass Review」串成系列</td>
          <td>TODO 註解講「之後 review」</td>
      </tr>
      <tr>
          <td>TDD（test-driven）</td>
          <td>RED-GREEN-REFACTOR 三步本身就是 multi-pass</td>
          <td>「重構是 optional」當 appendix</td>
      </tr>
      <tr>
          <td>Agile（process）</td>
          <td>Sprint review / retrospective 是 pillar</td>
          <td>「有空回顧一下」當 appendix</td>
      </tr>
  </tbody>
</table>
<p>每個 methodology 的設計都該檢查：<strong>multi-pass 是 pillar 還是 appendix？</strong></p>
<hr>
<h2 id="如何識別該升-pillar-但被當-appendix">如何識別「該升 pillar 但被當 appendix」</h2>
<p>訊號：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「最後再 review 一下」「有空再 polish」這類 disclaimer</td>
          <td>升成獨立 pillar / 原則</td>
      </tr>
      <tr>
          <td>Multi-pass 內容散在多個 reference 角落、沒有單一定位</td>
          <td>抽出 pillar、各 reference 引用</td>
      </tr>
      <tr>
          <td>Pillar 列表只 3 條（看似簡潔）、但實作中常忘 review</td>
          <td>缺 pillar、補上 multi-pass</td>
      </tr>
      <tr>
          <td>「第 1 輪原則」+「第 2 輪原則」分開兩個 SKILL</td>
          <td>合併、multi-pass 是同 SKILL 的多輪、不是兩個 SKILL</td>
      </tr>
      <tr>
          <td>文件結尾「最後注意事項」常被使用者引用為「我忘了」</td>
          <td>結構問題、移到 pillar</td>
      </tr>
  </tbody>
</table>
<p>每個訊號都是 <strong>multi-pass 的位置太低</strong>、結構壓力把它當作 optional。</p>
<hr>
<h2 id="升-pillar-後的設計四個必要元素">升 pillar 後的設計：四個必要元素</h2>
<p>把 multi-pass 升成 pillar、需要含這四個元素才完整：</p>
<h3 id="1-明示第-1-輪不追求完美">1. 明示「第 1 輪不追求完美」</h3>
<p>寫在 pillar 內容、第一句就講：「第 1 輪不要追求 perfect、預期會有未發現問題、設計第 2 輪去 catch」。</p>
<p>去掉「第 1 輪該寫對」的隱含預設、釋放認知資源。</p>
<h3 id="2-列出-n-輪的-frame-清單">2. 列出 N 輪的 frame 清單</h3>
<p>每輪用什麼 frame、catch 什麼。例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">輪 1：生成 — idea → 字
</span></span><span class="line"><span class="ln">2</span><span class="cl">輪 2：對意圖 — 跟原意對齊嗎
</span></span><span class="line"><span class="ln">3</span><span class="cl">輪 3：機會成本語氣 — 絕對主義詞翻成 trade-off
</span></span><span class="line"><span class="ln">4</span><span class="cl">輪 4：grep-ability — 關鍵字前置嗎
</span></span><span class="line"><span class="ln">5</span><span class="cl">輪 5：反例 / 邊界 — 何時不適用寫了嗎</span></span></code></pre></div><h3 id="3-何時可跳輪">3. 何時可跳輪</h3>
<p>不是所有情境都跑全輪。寫清楚「跳輪的合理情境」、避免「跑全輪 = 過度工程」的反彈。</p>
<h3 id="4-跨-frame-的不可替代性">4. 跨 frame 的不可替代性</h3>
<p>明示：<strong>輪 N 不能用「再跑一次輪 N-1」取代</strong> — 不同 frame 才能 catch 不同層。重複同 frame = 同類錯一直 miss。</p>
<hr>
<h2 id="反模式我自己會-review當-pillar-替代">反模式：「我自己會 review」當 pillar 替代</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">不該寫：「請務必在送出前自行 review。」
</span></span><span class="line"><span class="ln">2</span><span class="cl">應該寫：「此 methodology 的第 N 個 pillar 是 Multi-pass Review、含 1-5 輪 frame：⋯⋯」</span></span></code></pre></div><p>「自行 review」= L1 紀律（<a href="../external-trigger-for-high-roi-work/">#72</a>）= 預期失敗。</p>
<p>「列入 pillar + 列輪次 + 列 checklist」= L3-L5 結構性對策 = 結構強制執行。</p>
<hr>
<h2 id="套用到本系統的具體-case">套用到本系統的具體 case</h2>
<h3 id="case-1requirement-protocol-skill">Case 1：requirement-protocol skill</h3>
<ul>
<li><strong>現況</strong>：3 大支柱 + 6 大原則、multi-pass 散在「2 次門檻」「漸進驗證」「revert checkpoint」三條原則裡、沒明示</li>
<li><strong>應該</strong>：升第 4 支柱「Multi-pass Refinement」、把散落的多輪意涵集中</li>
</ul>
<h3 id="case-2compositional-writing-skill">Case 2：compositional-writing skill</h3>
<ul>
<li><strong>現況</strong>：3 大支柱 + 5 大原則、各 reference 結尾有「self-check」段（部分 multi-pass 跡象）</li>
<li><strong>應該</strong>：升第 6 原則「Re-read Pass」、引用 <a href="../writing-multi-pass-review/">#83</a> 的 5 輪 frame、各 reference 加「第 2 輪 review checklist」</li>
</ul>
<h3 id="case-3frontend-with-playwright-skill">Case 3：frontend-with-playwright skill</h3>
<ul>
<li><strong>現況</strong>：「漸進驗證」原則含 multi-pass、但跟「dogfood / 多輪測試」沒串連</li>
<li><strong>應該</strong>：補抽象層原則段、明示 multi-pass 跨「漸進驗證 → playwright dogfood → production observation」是同一條 spiral</li>
</ul>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>本卡是 #72 在 methodology 設計層的展現 — appendix-level 是 L1 紀律、pillar-level 是 L3-L5 結構</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>Methodology 設計這個動作本身就是 multi-pass 的對象 — 第一版 pillar 不對、要 review</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td>寫 methodology 文件本身要套 #83 — methodology 文件也是 writing</td>
      </tr>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>Pillar 的命名要跑 multi-pass naming review</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>寫 methodology 時、便利的寫法是「核心 3 條 + 細節塞 appendix」、跟「使用者實際需要 multi-pass 跑」不對齊</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>Pillar 不該過度膨脹、但「該升的內容沒升」是反向偏差、本卡是補 #43 的另一邊</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Methodology 文件結尾有「最後 review 一下」</td>
          <td>升 pillar</td>
      </tr>
      <tr>
          <td>Pillar 列表只 3 條、但 reference 多次提到「再過一次」</td>
          <td>缺 multi-pass pillar</td>
      </tr>
      <tr>
          <td>Multi-pass 內容散在 ≥ 3 個地方</td>
          <td>抽 pillar、各 reference 引用</td>
      </tr>
      <tr>
          <td>「進階使用者再 review」這類分級</td>
          <td>結構訊號錯位 — multi-pass 不是進階、是 baseline</td>
      </tr>
      <tr>
          <td>使用者反饋「我忘了 review」</td>
          <td>結構問題、不是紀律問題、升 pillar</td>
      </tr>
      <tr>
          <td>Reference 結尾 self-check 沒人用</td>
          <td>位置太尾、提升結構地位</td>
      </tr>
      <tr>
          <td>新 methodology 文件第一版</td>
          <td>預設加 multi-pass pillar、不是寫完才補</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：Methodology 設計的 pillar / appendix 切分<strong>不是內容深淺問題、是執行率問題</strong>。Pillar 層必跑、appendix 層不跑。把 multi-pass 視為「附帶」= 結構性確保它不被執行。<strong>真正必要的東西要升結構、不能藏在末尾</strong>。</p>
]]></content:encoded></item><item><title>Compositional Writing</title><link>https://tarrragon.github.io/blog/skills/compositional-writing/skill/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/skills/compositional-writing/skill/</guid><description>&lt;h2 id="compositional-writing">Compositional Writing&lt;/h2>
&lt;p>以 Zettelkasten（卡片盒筆記法）為核心的寫作方法論。將每段文字視為可重複組合的原子卡片，讓人類讀者與 AI 代理人都能以最小認知負擔找到答案。&lt;/p>
&lt;hr>
&lt;h2 id="core-pillars核心支柱">Core Pillars（核心支柱）&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>支柱&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Atomization&lt;/strong> 原子化&lt;/td>
 &lt;td>一段文字只承載一個概念，可獨立閱讀與重用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Explicit Intent&lt;/strong> 意圖顯性與層級貼合&lt;/td>
 &lt;td>讀者第一眼就看懂「為什麼在這裡、屬哪個抽象層級、該做什麼」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Searchability&lt;/strong> 可查詢性&lt;/td>
 &lt;td>人和 AI 都能用關鍵字 / grep / regex 快速定位&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="core-principles核心原則速查">Core Principles（核心原則速查）&lt;/h2>
&lt;p>讀者能在本區塊完成快速複習；需要具體應用時，依下方「觸發路由」讀對應情境 reference。&lt;/p>
&lt;h3 id="1-原子化atomization">1. 原子化（Atomization）&lt;/h3>
&lt;p>一張卡一個概念：能獨立理解、可跨情境重用。拆分依據是&lt;strong>認知負擔與情境匹配度&lt;/strong> — 讀者要同時記住的概念數、以及這張卡是否符合讀者當下的情境需求。常見的誤判依據是「行數」（卡太長就拆）、行數只反映表面字數、不反映概念數：一張 200 行的卡可能只講一個概念、一張 30 行的卡可能塞了三個概念。判別問題是「讀者要同時 hold 幾個概念才讀得懂這張卡」、超過 7 個就要拆。&lt;/p>
&lt;p>&lt;strong>拆分判準的核心問題&lt;/strong>：「這張卡聚焦在什麼問題、議題切完整了嗎？」— 判準是 &lt;strong>focus 完整度&lt;/strong>。常見的次級訊號是「卡之間是否衝突」「邊界是否清晰」、兩者都不夠：兩張卡互不衝突、仍可能各切了一半同樣議題；一張卡邊界清晰、仍可能塞了兩個獨立議題。focus 完整度問的是「這張卡有沒有把它聲稱要解決的議題講完」、是 contrast 上面那兩個訊號抓不到的死角。&lt;/p>
&lt;h3 id="2-索引建立indexing">2. 索引建立（Indexing）&lt;/h3>
&lt;p>用 MOC（Map of Content）、tag 層級與反向索引把卡片串成可導航的網。入口文件&lt;strong>只做路由&lt;/strong>、把細節留給目標卡；引用深度&lt;strong>最多一層&lt;/strong>、讓讀者一跳就到答案（避免 A→B→C 的多層跳躍）。&lt;/p>
&lt;p>&lt;strong>引用錨點用語意標題、不用位置編號&lt;/strong>：引用另一個章節 / 階段 / 條列項時寫「見核心問題」、不寫「見 Stage 3」— 編號是結構排列的 derivation、結構重排時引用句字面完好、語意 silent 指向錯的內容（比 broken link 難偵測：連結斷掉會報錯、編號錯位會成功解析到錯的東西）。對應要求是每個結構單位的標題要承載核心意義（「Stage 3：核心問題」、編號只作排序前綴）、引用取語意半邊；發布方凍結的編號（RFC 段號 / 法條）是 fact、可引用。詳見 &lt;a href="https://tarrragon.github.io/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">reference-by-semantic-title-not-number&lt;/a>。&lt;/p>
&lt;p>&lt;strong>語意錨用單一字串、引用他卡用對方的詞彙&lt;/strong>：同一個結構單位的語意名稱只能有一個 canonical 字串（取標題語意半邊）— 同義雙名（標題「決策記錄 + scaffold 建議」、引用「決策收斂階段」）讓 grep 掃 A 漏 B、重排修復退回人腦對應。引用另一張卡並描述它的內容時、寫之前把被引卡重新打開、用它自己的分類詞彙轉述 — 記憶存概念不存 taxonomy、憑印象轉述會把對方明確分開的類別併掉、每條關係宣告要找得到被引卡的支撐句。&lt;/p>
&lt;p>&lt;strong>集合命名用角色、不內嵌數量&lt;/strong>：標題要當穩定錨、就得先是純 fact —「核心七問」「成長六階段」「四大支柱」把成員數烤進名字、數量是成員清單的 derivation、加一問名稱先失真、所有複製過名稱的地方跟著過期。命名只承載角色與層級（核心問題 / 撞牆階段 / 支柱）、數量讓清單自己呈現；外部凍結品牌（SOLID 五原則 / OWASP Top 10）跟概念閾值（兩次門檻）的數字是 fact、可留。詳見 &lt;a href="https://tarrragon.github.io/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">name-collections-by-role-not-count&lt;/a>。&lt;/p>
&lt;h3 id="3-意圖顯性與層級貼合explicit-intent--layer-alignment">3. 意圖顯性與層級貼合（Explicit Intent &amp;amp; Layer Alignment）&lt;/h3>
&lt;p>&lt;strong>寫作前先標記本文所在抽象層級（實作 / 工具 / 協作 / 認知 / 架構）、論述停在該層&lt;/strong>。素材取自哪個層級、論述就收斂在哪個層級 — 因為跨層提升等於用 X 層的詞彙描述 Y 層的議題、讀者拿到規則但對不到自己當下的情境。要把實作層素材抽象到認知層、先補對應抽象層的支撐文件（讓論述有對應層的詞彙跟 case 可引用）、再做跨層提升。&lt;/p>
&lt;p>寫「為什麼」和「要達成什麼」、把「程式碼在做什麼」留給程式碼自身（程式碼讀一次就知道做什麼、寫進註解只是冗餘）。主詞與動詞直接、段落開頭即表達意圖。TODO / placeholder 留給 inline 註解、文件本體只放當前契約 — 因為文件常被當成「契約 SSoT」引用、混入未完成事項會讓讀者誤判契約範圍。同一篇文字貼合它在系統裡的抽象層級、把下層實作藏在介面後面。&lt;/p>
&lt;p>&lt;strong>機會成本語氣優先&lt;/strong>：程式設計大多是多目標取捨、討論的是「在什麼情境下哪個選項較划算」。把絕對二元語氣（「正確概念是 X / 替代方案不足 / 應該這樣做」）翻成情境化敘述：「比較好的做法是 A、因為 [情境] / B 在 [其他情境] 合理 / D 的成本特別高、只在 [極端情境] 才划算」。機會成本教讀者「思考方式」（能套用到新情境）、絕對主義教讀者「規則」（壓力下會忘）— 所以前者是預設語氣。例外保留給物理 / 法律 / 數學事實（安全性、數據完整性、合規、雜湊必有碰撞）。絕對二元語氣有兩種形式：&lt;strong>命令式&lt;/strong>（「應該做 X」）讀者聽得出是主張、會審；&lt;strong>必然式&lt;/strong>（「X 天生就是 Y / 本質就是 / 必然」）偽裝成事實陳述、更隱形 — 把設計選擇講成自然法則時尤其要 catch、還原成「在選了某前提後 X 才以此形式成立」。判別線：這個必然有沒有上游設計選擇當前提（有=條件性、要講前提；無=真必然、可斷言）。詳見 &lt;a href="https://tarrragon.github.io/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">teaching-prose-neutral-register&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<h2 id="compositional-writing">Compositional Writing</h2>
<p>以 Zettelkasten（卡片盒筆記法）為核心的寫作方法論。將每段文字視為可重複組合的原子卡片，讓人類讀者與 AI 代理人都能以最小認知負擔找到答案。</p>
<hr>
<h2 id="core-pillars核心支柱">Core Pillars（核心支柱）</h2>
<table>
  <thead>
      <tr>
          <th>支柱</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Atomization</strong> 原子化</td>
          <td>一段文字只承載一個概念，可獨立閱讀與重用</td>
      </tr>
      <tr>
          <td><strong>Explicit Intent</strong> 意圖顯性與層級貼合</td>
          <td>讀者第一眼就看懂「為什麼在這裡、屬哪個抽象層級、該做什麼」</td>
      </tr>
      <tr>
          <td><strong>Searchability</strong> 可查詢性</td>
          <td>人和 AI 都能用關鍵字 / grep / regex 快速定位</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="core-principles核心原則速查">Core Principles（核心原則速查）</h2>
<p>讀者能在本區塊完成快速複習；需要具體應用時，依下方「觸發路由」讀對應情境 reference。</p>
<h3 id="1-原子化atomization">1. 原子化（Atomization）</h3>
<p>一張卡一個概念：能獨立理解、可跨情境重用。拆分依據是<strong>認知負擔與情境匹配度</strong> — 讀者要同時記住的概念數、以及這張卡是否符合讀者當下的情境需求。常見的誤判依據是「行數」（卡太長就拆）、行數只反映表面字數、不反映概念數：一張 200 行的卡可能只講一個概念、一張 30 行的卡可能塞了三個概念。判別問題是「讀者要同時 hold 幾個概念才讀得懂這張卡」、超過 7 個就要拆。</p>
<p><strong>拆分判準的核心問題</strong>：「這張卡聚焦在什麼問題、議題切完整了嗎？」— 判準是 <strong>focus 完整度</strong>。常見的次級訊號是「卡之間是否衝突」「邊界是否清晰」、兩者都不夠：兩張卡互不衝突、仍可能各切了一半同樣議題；一張卡邊界清晰、仍可能塞了兩個獨立議題。focus 完整度問的是「這張卡有沒有把它聲稱要解決的議題講完」、是 contrast 上面那兩個訊號抓不到的死角。</p>
<h3 id="2-索引建立indexing">2. 索引建立（Indexing）</h3>
<p>用 MOC（Map of Content）、tag 層級與反向索引把卡片串成可導航的網。入口文件<strong>只做路由</strong>、把細節留給目標卡；引用深度<strong>最多一層</strong>、讓讀者一跳就到答案（避免 A→B→C 的多層跳躍）。</p>
<p><strong>引用錨點用語意標題、不用位置編號</strong>：引用另一個章節 / 階段 / 條列項時寫「見核心問題」、不寫「見 Stage 3」— 編號是結構排列的 derivation、結構重排時引用句字面完好、語意 silent 指向錯的內容（比 broken link 難偵測：連結斷掉會報錯、編號錯位會成功解析到錯的東西）。對應要求是每個結構單位的標題要承載核心意義（「Stage 3：核心問題」、編號只作排序前綴）、引用取語意半邊；發布方凍結的編號（RFC 段號 / 法條）是 fact、可引用。詳見 <a href="/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">reference-by-semantic-title-not-number</a>。</p>
<p><strong>語意錨用單一字串、引用他卡用對方的詞彙</strong>：同一個結構單位的語意名稱只能有一個 canonical 字串（取標題語意半邊）— 同義雙名（標題「決策記錄 + scaffold 建議」、引用「決策收斂階段」）讓 grep 掃 A 漏 B、重排修復退回人腦對應。引用另一張卡並描述它的內容時、寫之前把被引卡重新打開、用它自己的分類詞彙轉述 — 記憶存概念不存 taxonomy、憑印象轉述會把對方明確分開的類別併掉、每條關係宣告要找得到被引卡的支撐句。</p>
<p><strong>集合命名用角色、不內嵌數量</strong>：標題要當穩定錨、就得先是純 fact —「核心七問」「成長六階段」「四大支柱」把成員數烤進名字、數量是成員清單的 derivation、加一問名稱先失真、所有複製過名稱的地方跟著過期。命名只承載角色與層級（核心問題 / 撞牆階段 / 支柱）、數量讓清單自己呈現；外部凍結品牌（SOLID 五原則 / OWASP Top 10）跟概念閾值（兩次門檻）的數字是 fact、可留。詳見 <a href="/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">name-collections-by-role-not-count</a>。</p>
<h3 id="3-意圖顯性與層級貼合explicit-intent--layer-alignment">3. 意圖顯性與層級貼合（Explicit Intent &amp; Layer Alignment）</h3>
<p><strong>寫作前先標記本文所在抽象層級（實作 / 工具 / 協作 / 認知 / 架構）、論述停在該層</strong>。素材取自哪個層級、論述就收斂在哪個層級 — 因為跨層提升等於用 X 層的詞彙描述 Y 層的議題、讀者拿到規則但對不到自己當下的情境。要把實作層素材抽象到認知層、先補對應抽象層的支撐文件（讓論述有對應層的詞彙跟 case 可引用）、再做跨層提升。</p>
<p>寫「為什麼」和「要達成什麼」、把「程式碼在做什麼」留給程式碼自身（程式碼讀一次就知道做什麼、寫進註解只是冗餘）。主詞與動詞直接、段落開頭即表達意圖。TODO / placeholder 留給 inline 註解、文件本體只放當前契約 — 因為文件常被當成「契約 SSoT」引用、混入未完成事項會讓讀者誤判契約範圍。同一篇文字貼合它在系統裡的抽象層級、把下層實作藏在介面後面。</p>
<p><strong>機會成本語氣優先</strong>：程式設計大多是多目標取捨、討論的是「在什麼情境下哪個選項較划算」。把絕對二元語氣（「正確概念是 X / 替代方案不足 / 應該這樣做」）翻成情境化敘述：「比較好的做法是 A、因為 [情境] / B 在 [其他情境] 合理 / D 的成本特別高、只在 [極端情境] 才划算」。機會成本教讀者「思考方式」（能套用到新情境）、絕對主義教讀者「規則」（壓力下會忘）— 所以前者是預設語氣。例外保留給物理 / 法律 / 數學事實（安全性、數據完整性、合規、雜湊必有碰撞）。絕對二元語氣有兩種形式：<strong>命令式</strong>（「應該做 X」）讀者聽得出是主張、會審；<strong>必然式</strong>（「X 天生就是 Y / 本質就是 / 必然」）偽裝成事實陳述、更隱形 — 把設計選擇講成自然法則時尤其要 catch、還原成「在選了某前提後 X 才以此形式成立」。判別線：這個必然有沒有上游設計選擇當前提（有=條件性、要講前提；無=真必然、可斷言）。詳見 <a href="/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">teaching-prose-neutral-register</a>。</p>
<p><strong>選項數由議題本身的合理選項數決定</strong>：機會成本的精神是「教思考方式」 — 議題有幾個合理選項就寫幾個（2 個寫 A/B、3 個寫 A/B/C、4 個寫 A/B/C/D）。強湊到固定數量會把「教思考」退化成「填格式」、生出「實務上幾乎不存在」的低品質假反模式。真正的反模式直接標「D：反模式 — 違反 X 原則」、給讀者明確的「為什麼這條路該避開」、保持誠實。</p>
<p><strong>讀者定位聲明（生成端前置步驟）</strong>：每個教學模組在第一篇文章生成前，顯式聲明讀者定位——一段話描述目標讀者的背景、已有能力、缺的經驗。這份聲明是後續所有生成和 review 的可檢查基準。缺少顯式聲明時，LLM 預設用「教外行人」的姿態寫教學內容，這個預設不被 review 挑戰（reviewer 共享同一個預設），導致宣導語氣通過多輪審查。per <a href="/blog/report/review-lacks-outside-in-reader-frames/" data-link-title="多輪審查缺 outside-in 讀者 frame：六個系統性盲點" data-link-desc="review 框架的所有 frame 從已寫的內容出發（inside-out），缺從讀者完整需求出發的 frame（outside-in）。六個盲點全部由使用者而非 reviewer 發現：宣導語氣、管理層資訊缺失、接手情境遺漏、工具指引缺失、深度不拆分、讀者定位未預設。">outside-in reader frames report</a></p>
<p><strong>讀者定位：缺經驗的專業人士、不是外行人</strong>：技術教材的讀者是在特定領域缺乏經驗的專業人士，不是完全不懂的外行人。寫法是補足經驗缺口（直接描述情境與操作需求），不是從零科普（故事線導入、比喻堆疊、宣導語氣）。宣導式語氣（「你可能沒注意到」「把 X 想成 Y」「跑得好好的」）預設讀者無能、降低教材可信度。詳見 <a href="/blog/report/audience-is-professional-not-layperson/" data-link-title="讀者是缺經驗的專業人士、不是外行人" data-link-desc="技術教材的讀者定位應該是「在這個領域缺乏經驗的專業人士」，不是「完全不懂的外行人」。寫法是補足經驗缺口、不是從零科普。宣導式語氣（跑得好好的、你可能不知道）預設讀者無能，實際上會降低教材的可信度。">audience-is-professional-not-layperson</a>。</p>
<p><strong>跨專業溝通用情境遞進、不用比喻堆疊</strong>：向非本領域的專業人士（管理層、決策者）解釋技術議題時，減少術語並從簡單情境遞進到複雜情境。比喻傳遞形狀但不傳遞嚴重性、在細節處崩解、且隱含「對方聽不懂」的預設。用決策者熟悉的維度（影響範圍、恢復時間、成本量級）表達。詳見 <a href="/blog/report/cross-expertise-communication-scenario-not-analogy/" data-link-title="跨專業溝通用情境遞進、不用比喻堆疊" data-link-desc="向非本領域的專業人士解釋技術議題時，減少術語並從簡單情境遞進到複雜情境，比堆疊比喻有效。比喻傳遞形狀但不傳遞嚴重性；情境遞進讓對方用自己熟悉的決策框架（成本、風險、時間）消化資訊。">cross-expertise-scenario-not-analogy</a>。</p>
<p><strong>技術教材內嵌管理層可彙報的資訊</strong>：技術段落旁嵌入成本量級、時程估算、進度指標與決策簽核點（各 1-2 句），讓讀者學完技術做法的同時拿到向上彙報的素材。成本用量級不用精確數、時程用範圍不用單點、進度用可查詢指標。詳見 <a href="/blog/report/technical-content-needs-management-reportable-info/" data-link-title="技術教材要內嵌管理層可彙報的資訊" data-link-desc="技術文章的讀者不只要知道怎麼做，還要能向上彙報為什麼做、花多久、花多少。成本量級、時程估算、進度指標與需簽核的決策點應該嵌在技術段落旁邊，而非集中在另一篇溝通指南裡。">management-reportable-info-in-technical-content</a>。</p>
<p><strong>知識卡建卡判準用「最不熟悉的讀者」</strong>：知識卡的建卡判準是「目標讀者群裡最不熟悉的那端能不能理解這個術語」，不是「作者覺得夠不夠常見」。常識是相對於背景的——.htaccess 對 PHP 工程師是常識、對 Node.js 工程師完全陌生。跨背景讀者群的教材裡，幾乎所有領域特定術語都需要建卡。建卡的邊際成本低（40-50 行）、讀者缺卡的代價高（離開教材去 Google、可能找到不一致的解釋）。per <a href="/blog/report/common-knowledge-is-relative-to-reader-background/" data-link-title="常識是相對於讀者背景的、不是作者背景的" data-link-desc="知識卡的建卡判準不能用「這個夠不夠常見」——對 PHP 工程師是常識的 .htaccess，對 Node.js 工程師完全陌生；對後端工程師是常識的 DNS TTL，對前端工程師需要解釋。建卡看的是目標讀者群裡最不熟悉的那個人能不能理解，不是作者自己覺得夠不夠普遍。">常識是相對於讀者背景的</a>。</p>
<p><strong>操作步驟帶環境專屬工具路徑</strong>：操作型文章的每一步至少帶一條工具路徑（用什麼軟體、輸入什麼指令）。同一個動作在不同環境（container / VM / 共享主機）的工具路徑可能完全不同——「拍下現況」在 container 是 <code>docker commit</code>、在 VM 是 AMI 快照、在共享主機是 FTP mirror + phpinfo。文章涵蓋多種環境時、每一步要按環境分列工具、或標明適用環境。自測問題：「讀者坐在電腦前，下一個動作是打開什麼軟體？」答不出來就是缺口。per <a href="/blog/report/operational-how-needs-environment-specific-tooling/" data-link-title="操作指引的「怎麼做」要帶環境專屬的工具路徑" data-link-desc="操作型教材說「拍下現況」「匯出資料庫」「建立備份」時，不同執行環境（container / VM / 共享主機）的工具路徑完全不同。只寫動作不寫工具，讀者知道該做什麼但做不到。這個缺口在 fact-check 和 steelman 審查裡結構性隱形，因為動作本身在邏輯層是正確的。">操作指引要帶環境專屬工具路徑</a>。</p>
<p><strong>Case 引用段落的三段式結構</strong>：三段式是案例引用段落的順序紀律 — 把「概念 → 案例 → 操作」三層分開承擔（段首給概念定義、case 引用居中、通用工程知識展開）、讓段落結構跟讀者學習新概念的認知順序對齊。LLM 從 case 反推內容容易把 case 揭露當概念出發點、實證觀察 11/12 段都犯這個錯。詳見 <a href="/blog/report/case-citation-three-part-structure/" data-link-title="案例引用三段式段落結構：概念定義 → case 引用 → 通用展開" data-link-desc="Case 引用段落要走三段式結構：(1) 段首概念定義句先寫『該概念是什麼、承擔什麼責任』、(2) 第二位置 case 引用、(3) 通用工程知識展開；段首被 case 引用取代是 06 模組最大宗 systemic 違規（11/12 段都犯）；本卡跟 #115（引用深度）/ #116（內部分層）/ #117（跨 case 合成）正交、處理段落結構順序">case-citation-three-part-structure</a>。</p>
<p><strong>原子筆記要有向上的議題入口</strong>：承載知識的原子筆記（Zettelkasten 卡 / glossary / 術語條目）不是字典條目 — 字典答「這個詞是什麼」、承載知識答「你在討論什麼、撞到什麼問題、才需要這知識」。撰寫者有預設情境讀者沒有、所以每張卡（或其上層）要從情境進入而非劈頭給定義：建議題 hub（以讀者遇到的問題為題）討論再分流到原子卡、卡頂回指議題、讓搜尋直接落地者也有回路。沒這層卡淪字典、讀者沒有觸發點、不知何時用。詳見 <a href="/blog/report/atomic-note-needs-situational-entry/" data-link-title="原子筆記要有向上的議題入口：讀者要知道為何讀這張、何時會撞到" data-link-desc="承載知識的原子筆記不是字典條目。每張卡（或其上層）要回答「你在討論什麼議題、撞到什麼問題，才需要這個知識」——從情境進入，而非從定義進入。做法是建『議題 hub』上層筆記討論問題、再分流到術語卡，術語卡頂部回指議題。">atomic-note-needs-situational-entry</a>。</p>
<h3 id="4-可查詢性searchability">4. 可查詢性（Searchability）</h3>
<p>關鍵字前置、使用可 grep 的分隔符（<code>:</code> <code>|</code> <code>→</code> <code>==</code>）、欄位名稱使用 regex 友善格式。命名讓 AI 能以單次 grep 命中，不需要語意推理。</p>
<h3 id="5-欄位設計field-design">5. 欄位設計（Field Design）</h3>
<p>同一份文件的不同欄位，從不同角度觀察同一件事，不重複撰寫。<code>what</code> 描述動作、<code>why</code> 陳述動機、<code>acceptance</code> 定義可驗證條件；混淆欄位會讓讀者在多處讀到相同內容。</p>
<h3 id="6-多輪-re-read-passmulti-pass-review">6. 多輪 Re-read Pass（Multi-pass Review）</h3>
<p>完稿即進入 review 階段。一次寫對全部維度違反 working memory、實際結果是「每維度都做一半」。設計 N 輪 re-read、每輪用不同 frame：</p>
<table>
  <thead>
      <tr>
          <th>輪</th>
          <th>Frame</th>
          <th>抓什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>生成</td>
          <td>idea → 字、預期會有錯</td>
      </tr>
      <tr>
          <td>2</td>
          <td>對意圖（<a href="/blog/report/ease-of-writing-vs-intent-alignment/" data-link-title="寫作便利度跟意圖對齊反相關" data-link-desc="寫程式時最容易寫出的版本、通常是離意圖最遠的版本。便利度建立在「現有上下文 / 已 materialize 資料 / 已存在 API」上、而意圖對齊需要找到正確的層、處理上游、跨抽象層 — 兩者方向相反。識別這個反相關 = 識別自己掉進「容易寫的陷阱」。">ease-of-writing-vs-intent-alignment</a>）</td>
          <td>正文、title、description、MOC hook 都跟原意對齊</td>
      </tr>
      <tr>
          <td>3</td>
          <td>機會成本語氣</td>
          <td>全 surface 的絕對詞翻成 trade-off</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Grep-ability / 命名 / 術語</td>
          <td>title、slug、link label、段首關鍵字可單次 grep 命中；術語保留原文錨點與完整名詞頭</td>
      </tr>
      <tr>
          <td>5</td>
          <td>反例 / 邊界</td>
          <td>「何時不適用」段、反模式列表</td>
      </tr>
  </tbody>
</table>
<p>Surface enumeration 是 multi-pass 的固定前置步驟。寫作產物包含 body surface 與 metadata / navigation surface：<code>title</code>、<code>description</code>、<code>tags</code>、heading、link label、MOC / index entry、slug / filename。每輪 frame 都掃這份 surface 清單，讓正文與讀者入口共用同一個概念錨點。description / hook 對規則做壓縮時、<strong>可以丟細節、不可以改模態</strong> — 把本體的「條件允許（可延後但要記錄）」壓成「絕對禁止（不可跳過）」、讀者依摘要行動就會偏離本體；摘要讀起來比本體「更有力、更乾脆」就是失真訊號、模態詞跟主詞動詞同級、最後砍。實測一批七份文檔有四份的 description 出現模態漂移 — 這個檢查每批都要跑。</p>
<p><strong>核心</strong>：「再仔細一次」≠ multi-pass — 同 frame 重看 catch 不到新問題。每輪換 frame、才能 catch 不同層。各 reference（writing-articles / writing-code-comments / writing-documents / writing-prompts）依 output 類型有特化的輪次組合。</p>
<p>Naming 是這條原則最容易跳的子場景 — 第一版命名幾乎不對、四輪 review（第一版 / grep / cross-call-site / impl 洩漏）才收斂、見 <a href="/blog/report/naming-as-iterated-artifact/" data-link-title="Naming 是 iterated artifact：第一個名字幾乎不對、四輪 review 才收斂" data-link-desc="命名（變數 / 函式 / 檔名 / slug / API endpoint）幾乎沒有「一次寫對」的可能：第一個名字基於當下狹窄的 context、會在後續 cross-call-site / grep / 重構中暴露錯位。命名的正確設計是 iterated — 寫第一版 → grep-ability 測試 → cross-call-site 一致性 → impl 洩漏 → 重命名。本卡是 #83 在「命名」場景的特化。">naming-as-iterated-artifact</a> 跟 writing-code-comments 的 naming review 段。術語是 naming 的高歧義子場景：翻譯術語第一次出現保留原文錨點，中文壓縮術語保留完整名詞頭，中文名詞頭要保留來源中的概念角色，見 <a href="/blog/report/terminology-keeps-original-anchor/" data-link-title="術語翻譯要保留原文錨點" data-link-desc="翻譯術語時、中文名稱負責降低閱讀門檻，原文名稱負責鎖定概念邊界。只留中文會把 reader 帶進中文詞的日常歧義，只留英文會提高閱讀成本；中文後接英文括號是技術文章的穩定折衷。">terminology-keeps-original-anchor</a>、<a href="/blog/report/compressed-chinese-terms-need-head-noun/" data-link-title="中文壓縮術語要保留完整名詞頭" data-link-desc="中文技術寫作可以壓縮長詞，但不能省到只剩形容詞或單字修飾。像「多步驟 perplexity 盲」這類詞少了完整名詞頭，讀者無法判斷是在說盲點、盲區、盲測或失明比喻。壓縮後仍要能獨立回答「這是什麼」。">compressed-chinese-terms-need-head-noun</a> 與 <a href="/blog/report/translation-must-preserve-concept-role/" data-link-title="術語翻譯要保留概念角色" data-link-desc="術語翻譯不能只追求中文好讀，還要保留原詞在論證中的概念角色。Steelman 若翻成「最強版本測試」，reader 會以為它是一個檢查動作；但在決策語境裡，它更核心的責任是把反方論點重建成最強版本。">translation-must-preserve-concept-role</a>。</p>
<p><strong>高 stakes 內容追加輪 E（epistemic rigor、conditional opt-in）</strong>：reader 照做後錯誤不可逆的內容（資安 / concurrency 正確性 / distributed consistency / financial / medical）在 5 輪基本 frame 之外、追加 stakes 軸的 epistemic rigor pass——比照學術 peer review 跑 claim / evidence / method / threats / citation 五個 sub-check、加上 audit recommendation tier（accept / minor / major / withdraw）。一般內容 5 輪夠、不跑輪 E；高 stakes 內容兩軸都跑。詳見 <code>references/auditing-articles.md</code> 跟 <code>/report/writing-multi-pass-review/</code> 的「stakes-conditional 追加輪」段。</p>
<p><strong>Production 教學文章追加輪 8-10（字句層 catch、跑 N 輪仍漏時觸發）</strong>：跑了 5 輪基本 frame 仍系統性漏 catch 字句層問題（口語修辭 / 廢話前綴 / 地區漂移 / 依賴 code / <strong>裝飾符號 emoji</strong> / 對讀者喊話 / 自評誇飾 / 必然性框架 / 恐嚇式語氣 / 歸因語氣）時、追加三個換軸機制——輪 8 keyword bank（換工具、含 emoji / 裝飾 unicode 掃描）、輪 9 reader simulation（換視角、四 lens：自包含性 + register/stance + meta 殘留 + AI 歸因過度）、輪 10 self-criticism（換層次、審視 framework 本身覆蓋度）。短文 / 即時 note 不需要、production 教學文章在跑 5 輪後仍漏同類問題時 opt-in。<strong>keyword bank 命中是候選、不是判決</strong>——grep 命中後仍要一個語意判定步驟（這個命中是建立概念的違規、還是合規的反例對照 / hook），reviewer 容易把違規合理化放行；偵測（bank）跟判定（語意）是兩個認知步驟。<strong>register/stance 類（喊話 / 誇飾 / 必然）無穩定關鍵詞、keyword bank 抓不到、輪 9 reader-sim 是主 keyword bank 是輔、且最依賴 external cold-read</strong>。漏抓後補機制前先分 <strong>design gap</strong>（框架缺 frame、改框架）vs <strong>execution gap</strong>（框架有 frame 但只跑了臨時子集、改執行不是改框架）——「加 keyword」對 execution gap 跟無關鍵詞的類都無效。詳見 <a href="/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">multi-pass-review-frame-granularity</a>、<a href="/blog/report/visual-tool-error-layer-alignment/" data-link-title="視覺手段對齊錯誤層次：CSS / emoji 修不到語意 / 邏輯問題" data-link-desc="修視覺問題的工具（CSS、emoji、顏色、排版）只能擋視覺層、不能修語意 / 邏輯層。把語意 / 邏輯問題當成視覺問題修 = 蓋住症狀根因不動 &#43; false confidence、跟 #82 用 hook 蓋行為錯誤同骨。三層優先序：邏輯 → 語意 → 視覺、修法從深層往淺層走、不從症狀往回推。本卡是 #82 在「呈現層」的具體實例、是 #83 multi-pass review 缺的 vertical 軸。">decorative-symbols-keyword-bank</a>、<a href="/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">teaching-prose-neutral-register</a> 跟 <code>references/writing-articles.md</code> 輪 8-10 段。</p>
<p><strong>批量 sibling 寫作的生成端輪替</strong>：一次寫多份同類文檔時、cadence 同質化會在六個層發生（title 形式 / 開場句式 / 章節標題 / 敘事骨架 / 條目形態 / 跨檔引用句）、單份 review 全部抓不到、且 review 端抓過的同骨會在下一批復發 — 同類 finding 第二次出現、就把規則升到生成端：寫之前排好開場 frame 輪替（規則先行 / 後果先行 / 動作先行 / 反差先行）、條目形態輪替、敘事視角輪替、引用句去重。詳見 <a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">cadence-homogenization</a>。</p>
<p><strong>Instance 軸：跨 reviewer instance 隔離</strong>：Instance 軸是 multi-pass review 的另一條擴展軸 — N 個獨立 reviewer instance 各自獨立 context、各自跑 background、解「單一 reviewer 同時看多維度容易維度盲點 + context 污染」的問題。Instance 指獨立 reviewer 程式實體（如 agent tool spawn 出的 subagent）、跟同一 reviewer 換輪次 frame（frame 軸）正交可疊加。適用 production 教學文章 / 高 stakes 內容 / 跨章節教學模組這類維度複雜度高的審查場景。詳見 <a href="/blog/report/agent-team-context-isolation/" data-link-title="Agent team context 隔離設計：用不同 instance 換 frame、平行 background 保護主 context" data-link-desc="Multi-pass review 跨輪 frame（#83）跟跨 reviewer instance 隔離（本卡）是兩個 axis：#83 是同一 reviewer 換輪次 frame、本卡是不同 reviewer instance 各自獨立 background 跑；context 隔離設計讓主 context 只接精煉摘要、節省 ~80% token、跟同 reviewer 多輪 catch 同類錯（#114）形成互補解法">agent-team-context-isolation</a>。</p>
<p>詳見 <a href="/blog/report/writing-multi-pass-review/" data-link-title="Writing 的 multi-pass review：N 輪 review、每輪換 frame" data-link-desc="寫文章 / 註解 / 文件 / prompt 的「寫」不是單次動作 — 是 N 輪 review。第 1 輪生成、第 2 輪對意圖（#67）、第 3 輪檢查機會成本語氣、第 4 輪 grep-ability、第 5 輪反例 / 邊界。每輪不同 frame、單輪寫不出全部維度。本卡是 #82 在「寫」這個 output 動作的具體實例。">Writing 的 multi-pass review</a>、<a href="/blog/report/methodology-multi-pass-embedding/" data-link-title="Methodology 的 multi-pass 該升級為 pillar 層：核心結構才會被執行" data-link-desc="任何「教做事方法」的 methodology / SKILL / playbook、應該把 multi-pass refinement 放在 pillar / 核心原則層、不是放在末尾「附帶提醒」段。Pillar 層 = 結構性必跑、appendix 層 = 看心情選擇 = 永遠不跑。本卡是 #82 行為驗證 &#43; #72 結構性對策在「方法論設計本身」這一層的展現。">Methodology 的 multi-pass 該 embed 在 pillar</a>、<a href="/blog/report/metadata-surface-in-writing-review/" data-link-title="Metadata surface 要納入寫作 review 範圍" data-link-desc="寫作 review 的 surface 包含正文與 metadata surface：title、description、frontmatter、heading、link label、MOC 索引條。正文通過 positive wording 或 multi-pass review 只代表 body surface 收斂；讀者入口與索引入口也要跑同一套 frame，才能讓文章在第一眼、搜尋與跨篇路由上維持同一個概念錨點。">Metadata surface 要納入寫作 review 範圍</a>、<a href="/blog/report/false-sense-of-security-as-primary-failure/" data-link-title="False sense of security 是資安寫作的主要失敗模式" data-link-desc="資安教學內容的失敗模式不是「讀者學不到」、是「讀者以為學到了並照做、實際還有破口」。讀者實作後沒警覺 = 後續驗證、修補、事件偵測都不會被觸發、破口在生產系統長期 silent 累積。識別 false sense of security 句子的判準：讀者讀完後會說「我做了 X 防護所以安全」、卻無法回答「對什麼 threat 安全 / 什麼 deployment 條件 / 什麼前提失效」。">False sense of security 是高 stakes 寫作的主要失敗模式</a>、<a href="/blog/report/security-teaching-rigor-asymmetry/" data-link-title="資安教學的審查標準要對應風險不對稱" data-link-desc="一般教學寫不清楚、讀者學不到、損失停在學習端；資安教學寫不清楚、讀者照做後在系統上產生破口、損失轉嫁到生產端。兩者風險不對稱、審查嚴格度應該對應下游實作代價、不是讀者讀懂程度。資安內容的 audit bar 預設要拉到「讀者會 implement」、不是「讀者會 read」、否則所有寫作便利選擇（含糊敘述、省略邊界、引用而不驗證版本）都會 silent 變成實作端破口。">Risk-asymmetric audit standard</a>、<a href="/blog/report/colloquial-rhetoric-erodes-technical-precision/" data-link-title="口語化修辭在判斷工具型段落會稀釋技術精度" data-link-desc="技術文章的「判斷工具型段落」（讀者用來判斷自己 case 的論述）用「一輩子」「碰巧能用」「立刻撞牆」「沒事」這類口語修辭、會稀釋精度。修法是把口語修辭翻譯回技術屬性語言。但 hook / 引言 / narrative 段落用口語仍然合理——這是情境化的取捨、不是精度永遠優於可讀性。">colloquial-rhetoric-erodes-technical-precision</a>、<a href="/blog/report/prose-self-contained-without-code-reference/" data-link-title="商業邏輯論述要 self-contained：不依賴 code 才能被理解" data-link-desc="技術文章在「不放 code 的段落」仍然要 self-contained——商業邏輯論述不能預設讀者已經看過 code、用「那個 payload 第二段」「剛才的變數」這類 reference 等於把理解門檻轉嫁給讀者去翻 code。修法是把 reference 翻譯成「用名詞 / 角色 / 條件描述」的 self-contained 句子、即使讀者跳過所有 code block 也能理解論述。">prose-self-contained-without-code-reference</a>、<a href="/blog/report/regional-terminology-alignment/" data-link-title="地區用語對齊：寫作前先確定讀者的中文語料" data-link-desc="繁中 vs 簡中的用詞差異不只是字形（屏 / 螢幕、視頻 / 影片）、更是技術術語跟業務情境的精度差。寫作前要先確定讀者的中文語料、避免用對方語料中不存在或意思偏移的詞。常見漂移：硬體（屏 / 螢幕）、檔案系統（文件 / 檔案）、概念詞（默認 / 預設）、修辭詞（質量 / 品質）、混雜情境的英文中文比例。">regional-terminology-alignment</a>、<a href="/blog/report/multi-pass-review-frame-granularity-blindspot/" data-link-title="Multi-pass review 的 frame 顆粒度盲點：抽象規則 → 具體訊號的轉譯不完整" data-link-desc="Multi-pass review 跑了 4 輪、字句層問題（口語修辭 / 地區用語 / 依賴 code / 廢話前綴）仍漏 catch——揭露 frame 顆粒度盲點：抽象規則（如「機會成本語氣」「正向陳述」「最重要的話優先說」）沒被轉譯成具體訊號（如 grep keyword bank：「一輩子 / 碰巧 / 撞牆 / 下次 X 時 / 不是 A 而是 B」）。修法是把每條規則展開成可 grep 的 keyword bank、加 reader simulation 輪、加 self-criticism 輪。">multi-pass-review-frame-granularity</a>、<a href="/blog/report/design-flaw-by-current-axes-not-hindsight/" data-link-title="設計檢討用當下三軸論證、不依賴 hindsight" data-link-desc="本卡提倡用「當下成本對稱條件下選了限制更高的選項」當設計缺陷的判定方式、避免 hindsight 論述把需求演化誤判成設計缺陷。當下三軸論證（成本對稱性 / 可逆性 / 領域先驗）讓判斷不依賴結局發生、且歸因偏向工具預設與制度而非個人預見性。">design-flaw-by-current-axes-not-hindsight</a>、<a href="/blog/report/agent-team-context-isolation/" data-link-title="Agent team context 隔離設計：用不同 instance 換 frame、平行 background 保護主 context" data-link-desc="Multi-pass review 跨輪 frame（#83）跟跨 reviewer instance 隔離（本卡）是兩個 axis：#83 是同一 reviewer 換輪次 frame、本卡是不同 reviewer instance 各自獨立 background 跑；context 隔離設計讓主 context 只接精煉摘要、節省 ~80% token、跟同 reviewer 多輪 catch 同類錯（#114）形成互補解法">agent-team-context-isolation</a>、<a href="/blog/report/visual-tool-error-layer-alignment/" data-link-title="視覺手段對齊錯誤層次：CSS / emoji 修不到語意 / 邏輯問題" data-link-desc="修視覺問題的工具（CSS、emoji、顏色、排版）只能擋視覺層、不能修語意 / 邏輯層。把語意 / 邏輯問題當成視覺問題修 = 蓋住症狀根因不動 &#43; false confidence、跟 #82 用 hook 蓋行為錯誤同骨。三層優先序：邏輯 → 語意 → 視覺、修法從深層往淺層走、不從症狀往回推。本卡是 #82 在「呈現層」的具體實例、是 #83 multi-pass review 缺的 vertical 軸。">decorative-symbols-keyword-bank</a>、<a href="/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">teaching-prose-neutral-register</a>。</p>
<hr>
<h2 id="when-to-consult-this-skill觸發路由">When to Consult This Skill（觸發路由）</h2>
<table>
  <thead>
      <tr>
          <th>觸發情境</th>
          <th>讀哪份 reference</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>要寫或改一段程式碼註解 / doc comment</td>
          <td><code>references/writing-code-comments.md</code></td>
      </tr>
      <tr>
          <td>要起草 / 改寫一份文件（worklog、spec、README）</td>
          <td><code>references/writing-documents.md</code></td>
      </tr>
      <tr>
          <td>要設計 log / 錯誤訊息 / 結構化輸出</td>
          <td><code>references/writing-logs.md</code></td>
      </tr>
      <tr>
          <td>要撰寫給 AI 的 prompt / instruction / Agent 派發 / Ticket Context Bundle</td>
          <td><code>references/writing-prompts.md</code>（為 <code>.claude/rules/core/ai-communication-rules.md</code> 的詳細版庫，portability-allow）</td>
      </tr>
      <tr>
          <td>要撰寫完整長篇技術文章（blog post / post-mortem / 架構決策 / 除錯復盤 / 技術評估）</td>
          <td><code>references/writing-articles.md</code></td>
      </tr>
      <tr>
          <td>要把外部分析文章 / 產業評論 / 投資人備忘錄 / 高密度研究材料轉成教學型分析文章，或把 AI 改寫稿從摘要升級成可遷移框架</td>
          <td><code>references/source-to-teaching-analysis.md</code></td>
      </tr>
      <tr>
          <td>要翻譯 / 轉譯文章、把英文材料改寫成中文、檢查術語誤譯或中文譯名放回句子後是否成立</td>
          <td><code>references/translation-review.md</code></td>
      </tr>
      <tr>
          <td>要管理多篇相關文章的結構（系列、文集、知識庫、素材庫比例、MOC、跨篇引用、何時抽抽象層 / Pattern 卡片）</td>
          <td><code>references/managing-article-collections.md</code></td>
      </tr>
      <tr>
          <td>要對既有高 stakes 內容（資安 / concurrency / distributed / financial / medical）做 reviewer-style audit、找 false sense of security / 對位失效 / context 缺 / citation 過時</td>
          <td><code>references/auditing-articles.md</code></td>
      </tr>
      <tr>
          <td>要設計 ticket 欄位 / schema frontmatter / 表單欄位</td>
          <td><code>references/designing-fields.md</code></td>
      </tr>
      <tr>
          <td>想驗證寫作品質（認知負擔、獨立理解率）</td>
          <td><code>references/meta-metrics.md</code></td>
      </tr>
      <tr>
          <td>要新增或修改一份 Skill reference（撰寫品質規範、結構標準）</td>
          <td><code>references/reference-authoring-standards.md</code></td>
      </tr>
      <tr>
          <td>要驗收 Skill 發布品質（語意層驗收、Phase 2 dry-run）</td>
          <td><code>references/dry-run-guide.md</code></td>
      </tr>
  </tbody>
</table>
<p>每份 reference 自包含：以該情境為核心，把核心原則翻譯成可直接套用的檢查項與範例。閱讀任一 reference 不需要回來看其他 reference。</p>
<hr>
<h2 id="success-criteriam1-m2-認知負擔類">Success Criteria（M1-M2 認知負擔類）</h2>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>定義</th>
          <th>目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>M1 — 找到答案路徑</strong></td>
          <td>讀者從 SKILL.md 出發，需要開啟幾個檔案才能解決問題</td>
          <td>≤ 2</td>
      </tr>
      <tr>
          <td><strong>M2 — reference 獨立理解率</strong></td>
          <td>隨機挑一份 reference，不讀其他 reference 能否獨立套用</td>
          <td>100%</td>
      </tr>
  </tbody>
</table>
<p>詳細量測方式與自評表見 <code>references/meta-metrics.md</code>。M3-M5（token 類）保留未定，待實際範例累積後補足。</p>
<hr>
<h2 id="跟特化寫作流程的分工">跟特化寫作流程的分工</h2>
<p>本 skill 是 <em>單篇</em> 寫作的基礎方法、覆蓋 articles / comments / logs / prompts / fields 等 surface。當寫作對象是 <em>跨多章節的教學模組</em>（5+ 章、有案例庫支撐、跨章引用密集）、屬特化情境、有專屬的 <em>跨章節生產流程</em>：案例庫 audit 抽 findings、SSoT 對應規劃、agent team 平行 review、跨檔修正循環、跨章 polish pass。</p>
<p>兩類流程的分工：</p>
<table>
  <thead>
      <tr>
          <th>流程</th>
          <th>適用</th>
          <th>核心紀律</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>本 skill（compositional-writing）</strong></td>
          <td>單篇文字（articles / comments / logs / prompts / fields）</td>
          <td>6 原則（原子化 / 索引 / 意圖顯性 / 可查詢 / 欄位 / 多輪 review）+ 各 surface 特化 reference</td>
      </tr>
      <tr>
          <td>跨章節教學模組生產流程</td>
          <td>跨 5+ 章、有 case 庫的教學模組</td>
          <td>case-first 流程：案例 audit → 基於 findings 寫稿 → agent team 平行 review → 修正循環 → polish pass、加 case 引用四 axis 紀律（深度 / 分層 / 合成 / 結構）</td>
      </tr>
  </tbody>
</table>
<p>兩類流程互補疊加 — 教學模組的每章內部寫作仍套本 skill 6 原則、case 引用段落用 <a href="/blog/report/case-citation-three-part-structure/" data-link-title="案例引用三段式段落結構：概念定義 → case 引用 → 通用展開" data-link-desc="Case 引用段落要走三段式結構：(1) 段首概念定義句先寫『該概念是什麼、承擔什麼責任』、(2) 第二位置 case 引用、(3) 通用工程知識展開；段首被 case 引用取代是 06 模組最大宗 systemic 違規（11/12 段都犯）；本卡跟 #115（引用深度）/ #116（內部分層）/ #117（跨 case 合成）正交、處理段落結構順序">case-citation-three-part-structure</a>、agent team review 用 <a href="/blog/report/agent-team-context-isolation/" data-link-title="Agent team context 隔離設計：用不同 instance 換 frame、平行 background 保護主 context" data-link-desc="Multi-pass review 跨輪 frame（#83）跟跨 reviewer instance 隔離（本卡）是兩個 axis：#83 是同一 reviewer 換輪次 frame、本卡是不同 reviewer instance 各自獨立 background 跑；context 隔離設計讓主 context 只接精煉摘要、節省 ~80% token、跟同 reviewer 多輪 catch 同類錯（#114）形成互補解法">agent-team-context-isolation</a>。當下游專案沒有跨章節教學模組需求、本 skill 即可獨立運作；當有需求、教學模組生產流程是本 skill 的擴展層、不取代本 skill。</p>
<h2 id="跟-multi-round-review-的協同">跟 multi-round-review 的協同</h2>
<p>寫多篇章節 / report 卡 / knowledge card 後做<strong>多輪 agent reviewer audit</strong> 時、本 skill 應該跟 multi-round-review skill 同時啟動。觸發詞「多輪審查 / Round 1/2/3 / batch review / 寫作 audit」會同時啟動兩個 skill：</p>
<ul>
<li><strong>multi-round-review</strong> 規劃 frame 切換結構（Round 1 compliance / Round 2 cadence / Round 3 self-application）跟跨輪 finding 整合工作流</li>
<li><strong>本 skill（compositional-writing）</strong> 提供每輪 frame 的字句層 keyword bank — Round 1-A 寫作規範 reviewer 必須跑：
<ul>
<li><strong>正向陳述優先 grep</strong>：<code>rg &quot;不[行可是要能該支對符夠必]|無法|沒[做有]|而非|而不是&quot;</code>、加上<strong>否定起手定義句</strong>（原 pattern 漏「而是」、抓不到「不是 X、而是 Y」的後半）：<code>rg &quot;不是.{0,30}而是|不是.{0,20}、是|與其.{0,20}不如|不只.{0,15}更&quot;</code> — 主要敘述要正向、反例對照的少量負向可保留；判別在「核心概念第一次正面出現在句首、還是被擠到『而是』之後」</li>
<li><strong>口語修辭 grep</strong>：<code>rg &quot;其實|實務上|真的|碰巧|立刻撞牆|沒事&quot;</code></li>
<li><strong>地區用語 grep</strong>：<code>rg &quot;集群|默認|質量|視頻|函數|文件夾|接口&quot;</code></li>
<li><strong>廢話前綴 grep</strong>：<code>rg &quot;值得注意的是|需要說明的是|實際上|基本上|事實上&quot;</code></li>
<li><strong>裝飾符號 grep</strong>：<code>rg &quot;✅|❌|⚠️|🚨|🟡|🟢|⭐|📌|✓|✗&quot;</code></li>
<li><strong>對讀者喊話 grep</strong>：<code>rg &quot;很多人|大家|不少人|你天天|你會|你可能|先讀懂|先釐清|別搞混|別被&quot;</code> — 教材中性陳述、不安撫情緒 / 不第二人稱代入 / 不祈使控制閱讀（hook / narrative 段落輕度第二人稱可留）</li>
<li><strong>自評誇飾 grep</strong>：<code>rg &quot;教科書級|堪稱|可謂|完美|經典|範本級|大師級|漂亮地|優雅地|最佳實踐|best practice&quot;</code> — 品質 verdict 頂替技術理由、換成機制 / 條件</li>
<li><strong>必然性框架 grep</strong>：<code>rg &quot;天生|與生俱來|本質就是|本來就是|必然|唯一|註定|理所當然&quot;</code> — 把設計選擇講成自然法則、還原成條件性（物理 / 法律 / 數學事實除外）</li>
<li><strong>歸因語氣 grep</strong>：<code>rg &quot;承認|暴露了|證明了失敗|被迫&quot;</code> — 描述系統行為用「信號」「反映」「顯示」等中性觀測詞、避免「承認」「暴露」等責任歸因詞；「被迫」在描述外部強制約束時可保留</li>
<li><strong>宣導語氣 grep</strong>：<code>rg &quot;你可能沒注意|你可能不知道|想像一下|把.{1,5}想成|跑得好好的|聽起來很|其實很簡單|說穿了就是|等於拆未爆彈|乾瞪眼|延遲引爆&quot;</code> — 預設讀者無知或用情緒管理取代事實陳述；讀者是專業人士、直接描述情境與後果</li>
<li><strong>泛用詞濫用 grep</strong>：<code>rg &quot;坑|東西|搞|弄|處理一下|情況&quot;</code> — 同一個泛用詞蓋過不同具體情境時、依情境換精確詞（意外 / 陷阱 / 出問題 / 發生狀況）；命中密集且各指不同事才算違規、真泛指 / 引號引用 / 輕度 hook 合規；「坑」另有地區偏移面（某些地區高頻、某些少用）。見 <a href="/report/avoid-overused-generic-words/">avoid-overused-generic-words</a></li>
<li><strong>這些 grep 曝光候選、不做自動判定</strong>：命中後要不要算違規有品味核心；且 LLM reviewer 跟作者共享文體、同源自審對 register 類（否定起手 / 喊話 / 誇飾 / 概念前置）有結構上限 ——「不是 X、而是 Y」這種 LLM 高頻自產句型最容易全員放水。grep + 同源判定只負責曝光候選、register 層的真防線是文體異源視角（human cold-read 或 prompt 採「挑剔否定起手 / 概念後置」對抗姿態的 reviewer）、同源回報的「clean」不可當真</li>
</ul>
</li>
</ul>
<p>詳細各維度的判讀規則跟修法、見對應 reference（writing-articles / writing-documents 等）跟 <code>references/principles/</code> 內的 cadence-homogenization / colloquial-rhetoric / regional-terminology / decorative-symbols / multi-pass-review-frame-granularity 等卡。</p>
<p>協同要點：</p>
<ul>
<li>單獨用 multi-round-review、容易漏字句層 — reviewer prompt 列「規範遵循」但漏 grep 具體 pattern</li>
<li>單獨用本 skill、容易漏跨輪 frame 規劃 — 知道要檢查字句層、但缺「Round N+1 用什麼新 frame」結構</li>
<li>兩個 skill 一起啟動 — multi-round-review 給結構、本 skill 給每輪的 grep checklist</li>
</ul>
<p>寫作對象是「單篇 + 完稿前自己 review」時、用本 skill 第 6 原則（多輪 Re-read Pass）的 5 輪 frame 即可；寫作對象是「跨多篇 + agent reviewer 平行 audit」時、multi-round-review 接手結構規劃、本 skill 在 reviewer prompt 內被引用作為檢查清單。</p>
<hr>
<h2 id="directory-index">Directory Index</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">compositional-writing/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── SKILL.md                              # 本檔：核心原則速查 + 觸發路由
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">└── references/
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    ├── writing-code-comments.md          # 情境 1：程式碼註解
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    ├── writing-documents.md              # 情境 2：文件撰寫
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    ├── writing-logs.md                   # 情境 3：log 輸出
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    ├── writing-prompts.md                # 情境 4：prompt 撰寫
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    ├── writing-articles.md               # 情境 5：完整長篇技術文章
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    ├── source-to-teaching-analysis.md     # 情境 5a：外部分析材料 → 教學型分析文章
</span></span><span class="line"><span class="ln">10</span><span class="cl">    ├── translation-review.md             # 情境 5b：文章翻譯 / 轉譯的句內邏輯 review
</span></span><span class="line"><span class="ln">11</span><span class="cl">    ├── managing-article-collections.md   # 情境 5c：跨多篇文章的結構（三層、素材庫比例、MOC、Pattern 卡片）
</span></span><span class="line"><span class="ln">12</span><span class="cl">    ├── designing-fields.md               # 情境 6：欄位設計（含六欄位角度總表）
</span></span><span class="line"><span class="ln">13</span><span class="cl">    ├── designing-fields-ticket-6w.md     # 六欄位詳細範例：正確 + 混淆共 12 項（按需讀取）
</span></span><span class="line"><span class="ln">14</span><span class="cl">    ├── meta-metrics.md                   # 品質量化驗收（M1-M5）
</span></span><span class="line"><span class="ln">15</span><span class="cl">    ├── reference-authoring-standards.md  # Skill reference 撰寫品質規範
</span></span><span class="line"><span class="ln">16</span><span class="cl">    ├── dry-run-guide.md                  # Skill 發布前語意層驗收（Phase 2 dry-run 流程）
</span></span><span class="line"><span class="ln">17</span><span class="cl">    └── principles/                       # Skill 內部支撐型原則卡（含 terminology / naming / review / case-citation / agent-team 等原則）</span></span></code></pre></div><hr>
<h2 id="reading-order建議閱讀順序">Reading Order（建議閱讀順序）</h2>
<ol>
<li>第一次接觸 → 從本 SKILL.md 的「核心支柱 + 核心原則」讀起</li>
<li>進入實際寫作情境 → 依觸發路由讀對應 reference（只讀一份）</li>
<li>想驗證成果 → 讀 <code>meta-metrics.md</code> 做自評</li>
</ol>
<hr>
<p><strong>Last Updated</strong>: 2026-06-25
<strong>Version</strong>: 0.18.0 — 輪 9 reader-sim 加第四 lens「AI 歸因過度」（AI 生成內容系統性把通用 pattern 框為 AI 特有、縮窄適用範圍且背上無法證實的舉證負擔；判準：「AI」換成「作者」論點仍成立 → 改通用觀察）；提交自檢清單加第 4 個生成端自問句（AI 歸因測試）。</p>
<p><strong>Version</strong>: 0.18.0 — 新增「泛用詞濫用」字句層 frame（讀者回饋觸發：反覆用「坑」把不同情境壓成同一模糊標籤、繁中少用）：keyword bank 加 <code>rg &quot;坑|東西|搞|弄|處理一下|情況&quot;</code>、新增 principle 卡 <a href="/report/avoid-overused-generic-words/">avoid-overused-generic-words</a>（依情境換精確詞、跟 colloquial/regional/cadence 三卡的軸區分）、writing-articles 輪 8 清單同步；命中密集且各指不同事才違規、真泛指 / 引號 / 輕度 hook 合規
<strong>Version</strong>: 0.17.0 — keyword bank 新增歸因語氣 grep + 否定起手定義句 pattern；輪 8-10 描述補恐嚇式語氣 / 歸因語氣；移除 comment-qa-hook / worklog-format-check hook（職責已由其他機制覆蓋）；references 更新（atomic-note / teaching-prose / writing-articles / writing-documents）。</p>
<p><strong>Version</strong>: 0.16.0 — 從工具 opinion 文章的三輪審查 + 使用者回饋回流 6 張 report 卡（WRAP 分析後選混合方案）：(1) keyword bank 加歸因語氣 grep（<code>承認|暴露了|證明了失敗|被迫</code>）— 唯一有穩定關鍵詞的新 design gap；(2) <code>teaching-prose-neutral-register</code> 加第四類「恐嚇式語氣」（把讀者放在被警告位置、判別線是「你→我們」替換測試）；(3) writing-articles 輪 9 reader-sim 加第三 lens「meta 資訊 vs 內容」（涵蓋 meta-commentary 殘留 + 主題偏移兩個 gap）；(4) writing-articles 提交自檢清單加 3 個生成端自問句（恐嚇式 hook / meta 刪除測試 / 歸因語氣）。不新增 principle 卡（27 張已夠、新議題融入現有卡）、不增 SKILL.md 主體段落（密度飽和、改動集中在 keyword bank 一行 + 下游 reference）。</p>
<p><strong>Last Updated</strong>: 2026-06-11
<strong>Version</strong>: 0.15.0 — 對七張同批 report 卡（#157-#163 主題：語意錨 / 決策表 / 入口分流 / 跨 surface / 摘要模態 / 引用詞彙 / 欄位契約）跑三 reviewer audit 後的回饋：(1) 新增 principle 卡 <a href="/blog/report/cadence-homogenization-in-batch-writing/" data-link-title="Cadence 同質化是模板的隱形維度" data-link-desc="規範定義「模板」時通常只指內容欄位（規模對照、tripwire、失敗模式），忽略句型骨架 / 段首語 / 段末收尾語 / 表格前導句 / 過渡詞同樣是模板的一種；批量寫作時最易讓 cadence 同質化、單篇看起來都合規、連讀多篇才浮現預期化；51 vendor 都用「四件事 → 任一缺失就是 X 邊界的待補項目」是案例；自檢要 grep 首句 / 段末句 / 表格前導句、不是只看欄位">cadence-homogenization</a>（同時修復 SKILL.md 長期 dangling 的引用）— 六個同骨層實測清單 + 生成端輪替規則 + 「同類 finding 第二次出現升生成端」的升級原則（觸發：上一輪抓過的「判準句同模」在本批復發、擴到 4/7）；(2) 原則 6 surface enumeration 補 description 模態檢查（實測 4/7 份 description 模態漂移、其中一份把同批另一張卡才立的「候選」壓成「證據」）；(3) 原則 6 補批量 sibling 生成端輪替段；(4) 原則 2 補「語意錨單一字串 + 引用他卡用對方詞彙」段（關係宣告 28 條核對抓到 2 條：被引卡沒漏的宣稱成漏、對方的 navigation surface 被轉述成 metadata surface）。</p>
<p><strong>Last Updated</strong>: 2026-06-11
<strong>Version</strong>: 0.14.0 — multi-round review Round 1 的 self-application 修正：兩個 reviewer 從不同 frame 獨立抓到本 skill 自身殘留 count-bearing 名稱（convergence 訊號）。(1) 「Core Pillars（三大支柱）」→「（核心支柱）」、「Six Principles（六大原則速查）」→「Core Principles（核心原則速查）」、「五階段流程」→「case-first 流程」；(2) references 內「五大原則」全改「核心原則」— 這批字串在原則從 5 個長到 6 個之後就已經全部過期（SKILL.md 寫六大、references 寫五大）、是 name-collections-by-role-not-count 卡描述的失效模式在本 skill 的實證；(3) reference-by-semantic-title-not-number 卡的 ISO 邊界限定到版本年份（跨版改版會重編條款）。後續 Round 3 self-application sweep 抓到本條宣稱的漏網（writing-code-comments 的「五大寫作原則」）與另兩處 count 殘留（「五大 surface」「三大正交 axis」）、已一併清除；兩張新 principle 卡依 steelman 補強（#155 卡補「標題改名 vs 編號位移」斷裂等級差、#156 卡補數字記憶價值的誠實對沖與「內部宣告凍結」邊界）。</p>
<p><strong>Last Updated</strong>: 2026-06-11
<strong>Version</strong>: 0.13.0 — 0.12.0 的同日延伸：使用者指出「核心七問」「成長六階段」是另一層問題 — 引用端修好了、但錨點名稱本身內嵌成員數（七 / 六 是 membership 的 derivation）、加一問名稱先失真、所有複製過名稱的地方跟著過期；0.12.0 的原則 2 新段自己就用「見核心七問」當正面範例而未察覺、證明命名端與引用端是獨立檢查維度。(1) 原則 2 補「集合命名用角色、不內嵌數量」段；(2) 新增 principle 卡 <a href="/blog/report/name-collections-by-role-not-count/" data-link-title="集合命名用角色、不內嵌數量：「核心七問」的七是成員數的 derivation、加一問就全面失真" data-link-desc="「核心七問」「成長六階段」「四大支柱」這類名稱把成員數量烤進名字裡 — 數量是集合當前成員的 derivation、不是集合的語意身分；成員增減時名稱失真、且名稱是被複製最多次的字串、缺陷隨每次引用繁殖。修法：命名只承載角色與層級（核心問題 / 次要問題 / 撞牆階段）、數量讓清單自己呈現。本卡是 #155 的命名端 sibling（#155 修引用端、本卡讓「語意標題是穩定錨」的前提真正成立）、#44 SSoT 在名稱內容的實例、#84 命名檢驗的數量維度。">name-collections-by-role-not-count</a>（self-contained、含三種可留數字的邊界：外部凍結品牌 / 概念閾值 / 緊鄰清單行內計數、含命名端掃描 regex）；(3) reference-by-semantic-title-not-number 卡補 sibling 連結、0.12.0 三處「核心七問」範例全改「核心問題」；(4) writing-documents Principle 2 補命名端段落。</p>
<p><strong>Last Updated</strong>: 2026-06-11
<strong>Version</strong>: 0.12.0 — 從一份多階段訪談 skill 的階段重編號事故回流：跨檔引用寫成「Stage 3」「Stage 1-3」、流程從四階段改六階段後十多處引用 silent 錯位（字面完好、語意指向錯的階段）、grep 只能抓字面、人工逐處判讀仍漏修兩處。(1) 原則 2（索引建立）補「引用錨點用語意標題、不用位置編號」段 — 編號是結構排列的 derivation、misdirected 比 dangling 難偵測、標題要承載可被引用的語意、凍結編號（RFC / 法條）是 fact 例外；(2) 新增 principle 卡 <a href="/blog/report/reference-by-semantic-title-not-number/" data-link-title="引用章節用語意標題、不用位置編號：編號是結構排列的 derivation、會隨版本漂移" data-link-desc="跨段落、跨檔引用結構單位（章節 / 階段 / 條列項）時、引用語意標題（副標題）、不引用位置編號（Stage 3、第 5 章、第 3 點）。編號是「目前結構排列」的 derivation、不是 fact；結構重排時編號全部位移、引用點不會報錯、而是 silent 指向錯的內容 — 比 broken link 更難偵測。標題的存在意義就是承載可被引用的語意。是 #44 SSoT 在結構引用維度的實例、#93 identifier-as-fact 家族的 sibling、#84 命名承載語意的引用面延伸。">reference-by-semantic-title-not-number</a>（self-contained、含重排 commit 的引用面掃描 regex）；(3) writing-documents Principle 2 cross-reference 段補同主題小節 + anti-pattern 表加「See Stage 3 指向活文件」列。同一問題第二次出現（v0.9.1 曾修過「Stage 1-5」→「五階段流程」的 portability leak）、符合兩次門檻立卡。</p>
<p><strong>Last Updated</strong>: 2026-06-01
<strong>Version</strong>: 0.11.0 — 從一篇技術教材 review 抽出三類字句層 register/framing 問題回流：(1) keyword bank 加 3 類（對讀者喊話 / 自評誇飾 / 必然性框架）、同步 description、協同段 grep、輪 8-10 段、writing-articles 輪 8；(2) 原則三補「絕對二元語氣的命令式 vs 必然式」subtype（必然式偽裝成事實、更隱形）；(3) 新增 principle 卡 <a href="/blog/report/teaching-register-states-not-addresses-reader/" data-link-title="教材用中性陳述、不對讀者喊話" data-link-desc="教材的 register 是中性陳述概念、不是對讀者說話。三種對讀者喊話的形式 —— 安撫情緒（很多人卡在）、第二人稱代入（你天天寫）、祈使控制閱讀（先讀懂 / 別搞混）—— 表面不同、共同違反是把讀者當成要管理的對話對象、而非把概念講清楚。問題不在精度（「你天天寫的 int count」精度完全正確）、在 stance。修法是換成中性陳述（常見的 int count）或描述性名詞標題（簽章的型別與名字拆解）。邊界：hook / narrative 段落的輕度第二人稱可幫讀者進入、不一律禁。">teaching-prose-neutral-register</a>（涵蓋三類、self-contained）；(4) multi-pass-review-frame-granularity 補「偵測之後：keyword bank 命中是候選不是判決」判定層段（偵測 vs 判定兩步驟、clean 可能是判定放水）。跟 multi-round-review Round 1-A 同步加 3 grep + 判定指引。</p>
<p><strong>Last Updated</strong>: 2026-05-27
<strong>Version</strong>: 0.10.0 — 從 13 張 knowledge cards 批量改寫負向表述的經驗回流：(1) description 加觸發詞「多輪審查 / multi-round review / batch review / 寫作 audit / 正向陳述 / 口語修辭 / 字句層 grep」、明示「也在 multi-round-review 啟動時觸發」；(2) 新增「跟 multi-round-review 的協同」段、列出 Round 1-A 寫作規範 reviewer 必須跑的 5 個 grep pattern（正向陳述 / 口語修辭 / 地區用語 / 廢話前綴 / 裝飾符號）、明示兩 skill 垂直協同關係；(3) 修正 multi-round-review 漏抓字句層的盲區、跟 multi-round-review v1.1 同步 cross-trigger 設計
<strong>Version</strong>: 0.9.2 — 從 business case-analyses 演變回流：新增 <code>source-to-teaching-analysis.md</code> 路由，處理外部分析文章 / 產業評論 / 投資人備忘錄到教學型分析文章的轉換；新增三張 principle（external-analysis-source-layering / cross-domain-reader-level-alignment / analysis-rewrite-delivers-transferable-framework），把 source 分層、跨領域讀者降層、可遷移框架交付從 blog report 抽成 portable 規則。
<strong>Version</strong>: 0.9.1 — Stage 4 修正 3-reviewer 抓的 33 issue：(1) #120 mirror 縮 scope 解過載（移除四 axis 表 / 句構分流 / polish pass 段、聚焦三段式結構 axis）+ 結論段首改概念定義句解 dogfooding 失敗；(2) #121 mirror 結論表三欄重設計（設計選擇 / 解決問題 / 失敗模式）+ 實作 pattern 縮成 abstract pattern；(3) 兩 mirror 角色段引用點改措辭（移除虛假引用宣告）；(4) SKILL.md 原則 3/6 兩補強段段首改概念定義句、原則 6「詳見」list 補新 mirror、Directory Index 補；(5) Portability leak 修：「Stage 2 自查清單」→「寫稿後段落自查清單」、「Stage 1-5」→「五階段流程」；(6) 五大 / 六大原則 drift 對齊（line 105 / 160）；(7) 既有 principles（writing-multi-pass-review / multi-pass-review-frame-granularity / ease-of-writing-vs-intent-alignment）補回引新 mirror、形成雙向 cross-link
<strong>Version</strong>: 0.9.0 — 從跨章節教學模組生產經驗回流：原則 3 補「Case 引用段落三段式結構」段（詳見 case-citation-three-part-structure）；原則 6 補「Instance 軸：跨 reviewer instance 隔離」段（詳見 agent-team-context-isolation、跟 frame 軸正交可疊加）；新增「跟特化寫作流程的分工」段（明示本 skill 是單篇基礎方法、跨章節教學模組生產流程是擴展層）；principles/ 新增兩張 mirror 卡（case-citation-three-part-structure / agent-team-context-isolation）、自包含、不引用外部 skill 或 blog content
<strong>Version</strong>: 0.8.1 — 第 6 原則同步 writing-articles v0.8.1：補「Production 教學文章追加輪 8-10」段（換工具 / 換視角 / 換層次三機制處理「跑 N 輪仍漏」字句層問題）；「詳見」連結加 5 張新 principle（colloquial-rhetoric / prose-self-contained / regional-terminology / multi-pass-review-frame-granularity / design-flaw-by-current-axes）
<strong>Version</strong>: 0.7.4 — 新增 <code>translation-review.md</code> 路由：翻譯 / 轉譯文章時，用句內邏輯檢查譯名是否跟主詞、動詞、修飾語、因果與讀者追問方向對位。
<strong>Version</strong>: 0.7.3 — managing-article-collections 補「素材庫比例」路由：多篇文章需要案例 / source / scenario / pattern 支撐時，主文章情境維持少量、素材庫保留 2-3 倍來源做反向驗證
<strong>Version</strong>: 0.7.2 — 補 multi-pass 的 surface 軸：review 先列 body / metadata / navigation surface（title、description、tags、heading、link label、MOC hook、slug / filename），每輪 frame 都掃同一份 surface 清單；新增內部 principle <code>metadata-surface-in-writing-review.md</code>
<strong>Version</strong>: 0.22.0 — 原則 3 加「知識卡建卡判準用最不熟悉的讀者」；常識是相對於讀者背景的、跨背景讀者群幾乎所有領域特定術語都需要建卡
<strong>Version</strong>: 0.21.0 — 原則 3 加「操作步驟帶環境專屬工具路徑」（同動作在 container/VM/共享主機的工具不同）
<strong>Version</strong>: 0.20.0 — 原則 3 加「讀者定位聲明」生成端前置步驟；從 infra 模組 retrospective 抽出（讀者定位未預設導致宣導語氣通過三輪審查）
<strong>Version</strong>: 0.19.0 — 新增三張 principle 卡（audience-is-professional-not-layperson / cross-expertise-scenario-not-analogy / management-reportable-info-in-technical-content）、原則 3 加讀者定位與跨專業溝通子原則、keyword bank 加宣導語氣 grep；從 infra 教學模組的寫作 retrospective 抽出
<strong>Version</strong>: 0.7.0 — Phase B1 結構升級：加第 6 原則「多輪 Re-read Pass」（明示 5 輪 frame）、引用 #83 / #84 / #85 multi-pass 系列。後續 Phase B2 會把各 reference 結尾加「第 2 輪 review checklist」段
<strong>Version</strong>: 0.6.0 — 從 references 過載的反思：writing-articles.md 從 780 行瘦身到 ~530 行（拆分判準 / 三類 structure 模板搬到 managing-article-collections.md、focus 集中在「單篇文章內部」）；新增規則八「自我應用 (dogfooding)」（教某條規則的段落本身遵守該規則）；managing-article-collections.md 整合「拆分判準」+「三層 structure 詳細對照 + 模板」；meta-metrics.md M2 加 dogfooding 失敗訊號
<strong>Version</strong>: 0.5.0 — 從批量改寫 35 篇的經驗回流：原則 3 補「選項數由議題決定、不強湊」（避免 A/B/C/D 強迫症與「實務上幾乎不存在」的假反模式）；writing-articles.md 新增規則九（三類文章 structure 模板）；managing-article-collections.md 新增「跨篇引用 idiom 庫」與「三層 structure 對照」
<strong>Version</strong>: 0.4.0 — 新增 <code>managing-article-collections.md</code>（跨多篇文章結構：三層、MOC、Pattern 卡片）；強化原則 1「原子化」（focus 是議題完整度、不是邊界清晰）；強化原則 3「意圖顯性」（機會成本語氣、不用絕對主義）
<strong>Version</strong>: 0.3.0 — 新增 <code>dry-run-guide.md</code> 於 Directory Index 與觸發路由（Skill 發布前語意層驗收 Phase 2 dry-run）</p>
]]></content:encoded></item><item><title>Case-First + Agent Team Review：教學內容的生產流程</title><link>https://tarrragon.github.io/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/case-first--agent-team-review%E6%95%99%E5%AD%B8%E5%85%A7%E5%AE%B9%E7%9A%84%E7%94%9F%E7%94%A2%E6%B5%81%E7%A8%8B/</guid><description>&lt;h2 id="這篇要說什麼">這篇要說什麼&lt;/h2>
&lt;p>寫教學文章時、純靠 LLM 自生內容會踩到兩個系統性盲點：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Scope 盲點&lt;/strong>：內容停在「教科書級」結構、漏掉真實事故才會浮現的失敗模式跟設計取捨。&lt;/li>
&lt;li>&lt;strong>準確性盲點&lt;/strong>：把通用 best practice 包裝成「[case] 揭露」、把案例沒講的細節寫成案例事實。&lt;/li>
&lt;/ol>
&lt;p>本文整理在 backend/01 至 backend/07 batch 1 七個模組撰寫過程中浮現的五階段流程：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>完整閱讀案例庫、抽 findings&lt;/strong> — 用案例驅動「該寫什麼」、不只是 LLM 自生&lt;/li>
&lt;li>&lt;strong>基於 findings 建立內容&lt;/strong> — findings 分布到章節、避免硬塞模板&lt;/li>
&lt;li>&lt;strong>Agent team 平行多輪審查&lt;/strong> — 用 3 個專責 reviewer 補 LLM 自盲點&lt;/li>
&lt;li>&lt;strong>修正循環&lt;/strong> — 按檔案批次修 high + 重要 medium、reviewer 抓出問題各章節對應修&lt;/li>
&lt;li>&lt;strong>Polish pass&lt;/strong> — 跨檔系統性 pattern 集中處理（負向骨架掃描、編號漂移、用語不一、cross-link 補漏）&lt;/li>
&lt;/ol>
&lt;p>實作數據：7 個模組（backend/01-07 batch 1）、~45 章 / 385 個 review issue、case fidelity 落在 70-93% 區間、修正後品質升至 0 critical 編造、cross-link 全綠、規範違反 polish pass 後降到單位數低 issue。06 模組後方法論工具化為可觸發 skill、stage 1-5 流程跟 reviewer prompt template、self-scan regex 都封裝成可重用元件。07 模組驗證下「章節已有 routing layer skeleton」的特殊處理（在現有結構內補 case-driven 深化段、不擴成厚重章節）。&lt;/p>
&lt;h2 id="問題llm-自生內容的兩個盲點">問題：LLM 自生內容的兩個盲點&lt;/h2>
&lt;p>純靠 LLM 寫教學章節、容易產出兩種品質風險：&lt;/p>
&lt;p>&lt;strong>Scope 盲點&lt;/strong>：LLM 從訓練資料抽出的內容偏 &lt;em>普遍性&lt;/em>、是「教科書 + 部落格 + 文件」的綜合。但真實工程議題的判讀條件常常來自 &lt;em>特定事故揭露&lt;/em>、不是普遍知識。例：&lt;/p>
&lt;ul>
&lt;li>「DynamoDB GSI 在 backfill 完成前查不到完整資料」這種具體陷阱&lt;/li>
&lt;li>「Super Bowl +50% no sweat 的工程意義是 headroom 提前預留、不是 vendor 神奇」這種反直覺判讀&lt;/li>
&lt;li>「99.99% → 99.999% 是指數成本、遠超直覺的 10x 線性想像」這種規模對照&lt;/li>
&lt;/ul>
&lt;p>純技術知識推導不出來、要看真實案例才會浮現。&lt;/p>
&lt;p>&lt;strong>準確性盲點&lt;/strong>：LLM 寫到「對應 [case]」時、容易把通用 best practice 包裝成案例事實、或把案例沒提到的細節擴寫成「案例揭露」。例（從本文討論的實作中抓出的真實 issue）：&lt;/p>
&lt;ul>
&lt;li>Snowflake 案例描述「異常查詢偵測維度（query 體積 / IP / 跨 schema scan）」、LLM 自生內容寫成「query 體積從 1MB / 天跳到 10GB / 天、來源 IP 從 office network 變 unknown VPS」— 具體數字是 LLM 加上去的、案例沒寫&lt;/li>
&lt;li>Tixcraft 案例策略段建議「composite key」、LLM 自生內容寫成「Tixcraft 用 user_id 分散、不是 event_id」— 案例沒揭露 Tixcraft 實際 partition key 設計&lt;/li>
&lt;/ul>
&lt;p>這兩類盲點都不容易在 self-review 時抓到、因為 LLM 看不出自己內容是否真的對應案例。&lt;/p>
&lt;h2 id="階段-1完整閱讀案例庫抽-findings">階段 1：完整閱讀案例庫、抽 findings&lt;/h2>
&lt;h3 id="為什麼要完整閱讀不能只看-title--description">為什麼要完整閱讀、不能只看 title + description&lt;/h3>
&lt;p>只看 title + description 能做 &lt;em>承接&lt;/em>（建立 link）、但無法做 &lt;em>scope 擴展&lt;/em>（揭露 LLM 不會自生的議題）。case 的 findings 通常埋在 body 的「判讀」段、不在 description 裡。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要說什麼">這篇要說什麼</h2>
<p>寫教學文章時、純靠 LLM 自生內容會踩到兩個系統性盲點：</p>
<ol>
<li><strong>Scope 盲點</strong>：內容停在「教科書級」結構、漏掉真實事故才會浮現的失敗模式跟設計取捨。</li>
<li><strong>準確性盲點</strong>：把通用 best practice 包裝成「[case] 揭露」、把案例沒講的細節寫成案例事實。</li>
</ol>
<p>本文整理在 backend/01 至 backend/07 batch 1 七個模組撰寫過程中浮現的五階段流程：</p>
<ol>
<li><strong>完整閱讀案例庫、抽 findings</strong> — 用案例驅動「該寫什麼」、不只是 LLM 自生</li>
<li><strong>基於 findings 建立內容</strong> — findings 分布到章節、避免硬塞模板</li>
<li><strong>Agent team 平行多輪審查</strong> — 用 3 個專責 reviewer 補 LLM 自盲點</li>
<li><strong>修正循環</strong> — 按檔案批次修 high + 重要 medium、reviewer 抓出問題各章節對應修</li>
<li><strong>Polish pass</strong> — 跨檔系統性 pattern 集中處理（負向骨架掃描、編號漂移、用語不一、cross-link 補漏）</li>
</ol>
<p>實作數據：7 個模組（backend/01-07 batch 1）、~45 章 / 385 個 review issue、case fidelity 落在 70-93% 區間、修正後品質升至 0 critical 編造、cross-link 全綠、規範違反 polish pass 後降到單位數低 issue。06 模組後方法論工具化為可觸發 skill、stage 1-5 流程跟 reviewer prompt template、self-scan regex 都封裝成可重用元件。07 模組驗證下「章節已有 routing layer skeleton」的特殊處理（在現有結構內補 case-driven 深化段、不擴成厚重章節）。</p>
<h2 id="問題llm-自生內容的兩個盲點">問題：LLM 自生內容的兩個盲點</h2>
<p>純靠 LLM 寫教學章節、容易產出兩種品質風險：</p>
<p><strong>Scope 盲點</strong>：LLM 從訓練資料抽出的內容偏 <em>普遍性</em>、是「教科書 + 部落格 + 文件」的綜合。但真實工程議題的判讀條件常常來自 <em>特定事故揭露</em>、不是普遍知識。例：</p>
<ul>
<li>「DynamoDB GSI 在 backfill 完成前查不到完整資料」這種具體陷阱</li>
<li>「Super Bowl +50% no sweat 的工程意義是 headroom 提前預留、不是 vendor 神奇」這種反直覺判讀</li>
<li>「99.99% → 99.999% 是指數成本、遠超直覺的 10x 線性想像」這種規模對照</li>
</ul>
<p>純技術知識推導不出來、要看真實案例才會浮現。</p>
<p><strong>準確性盲點</strong>：LLM 寫到「對應 [case]」時、容易把通用 best practice 包裝成案例事實、或把案例沒提到的細節擴寫成「案例揭露」。例（從本文討論的實作中抓出的真實 issue）：</p>
<ul>
<li>Snowflake 案例描述「異常查詢偵測維度（query 體積 / IP / 跨 schema scan）」、LLM 自生內容寫成「query 體積從 1MB / 天跳到 10GB / 天、來源 IP 從 office network 變 unknown VPS」— 具體數字是 LLM 加上去的、案例沒寫</li>
<li>Tixcraft 案例策略段建議「composite key」、LLM 自生內容寫成「Tixcraft 用 user_id 分散、不是 event_id」— 案例沒揭露 Tixcraft 實際 partition key 設計</li>
</ul>
<p>這兩類盲點都不容易在 self-review 時抓到、因為 LLM 看不出自己內容是否真的對應案例。</p>
<h2 id="階段-1完整閱讀案例庫抽-findings">階段 1：完整閱讀案例庫、抽 findings</h2>
<h3 id="為什麼要完整閱讀不能只看-title--description">為什麼要完整閱讀、不能只看 title + description</h3>
<p>只看 title + description 能做 <em>承接</em>（建立 link）、但無法做 <em>scope 擴展</em>（揭露 LLM 不會自生的議題）。case 的 findings 通常埋在 body 的「判讀」段、不在 description 裡。</p>
<p>實作中的對照：第一輪 audit 6 個 case、每 case 平均揭露 2.3 個 finding；其中約 7 成是 description 跟 title 看不到、要讀完整 body 才能抽出。例如 DraftKings 案例的「讀寫雙峰錯位」（比賽中讀爆量、payout 時寫爆量）— description 只說「financial ledger」、要讀「核心負載形狀」段才看到雙峰結構。</p>
<h3 id="邊際遞減的判斷">邊際遞減的判斷</h3>
<p>不是所有 case 都要讀。實作中觀察到的遞減曲線：</p>
<table>
  <thead>
      <tr>
          <th>輪次</th>
          <th>讀案例數</th>
          <th>揭露 findings</th>
          <th>平均 / case</th>
          <th>純新議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第一輪</td>
          <td>6</td>
          <td>14</td>
          <td>2.3</td>
          <td>~95%</td>
      </tr>
      <tr>
          <td>第二輪</td>
          <td>5</td>
          <td>15</td>
          <td>3.0</td>
          <td>~85%</td>
      </tr>
      <tr>
          <td>第三輪</td>
          <td>5</td>
          <td>13</td>
          <td>2.6</td>
          <td>~60%</td>
      </tr>
  </tbody>
</table>
<p>第三輪開始 <em>純新議題</em> 比例下降、重複 frame 出現（vendor dogfood 在 3 個 case 都揭露、benchmark 對照基準在 3 個 case 都揭露）。這是停止 audit 的訊號。</p>
<p>判讀條件：</p>
<ul>
<li><strong>繼續 audit</strong>：每 case 至少 1.5 個純新議題、且重複 frame 不超過 30%</li>
<li><strong>停止 audit</strong>：純新議題 &lt; 1 個 / case、重複 frame &gt; 50%、累積 finding 數已涵蓋目標章節主要議題</li>
</ul>
<p>實作中 11/94 cases（~12%）時邊際遞減訊號明顯、16/94 cases（~17%）時停止 audit、抽出 ~42 個 unique findings、足以支撐 6 個章節的 scope 擴展。</p>
<h3 id="findings-抽取方法">Findings 抽取方法</h3>
<p>讀 case 時、把每個段落看成可能的 finding 來源、問三個問題：</p>
<ol>
<li><strong>這段揭露什麼判讀條件</strong>？（是不是純技術推導不易浮現的議題）</li>
<li><strong>這段揭露什麼數字 / 設計細節</strong>？（規模、percentile、partition key 數量、replication lag 量級）</li>
<li><strong>這段揭露什麼失敗模式</strong>？（事故當下會出什麼問題、有什麼反直覺結論）</li>
</ol>
<p>寫進 findings 列表時、要附上 <em>case 來源</em> 跟 <em>該對應到哪個章節</em>。例：</p>
<blockquote>
<p>Finding: 線性擴展是 OLTP 設計最高目標、coordinator 是傳統 OLTP 的擴展瓶頸
來源: 9.C10 Spanner 案例「2 nodes → 45K reads/sec, 4 nodes → 90K reads/sec」段
章節: 1.11 全球分散式 OLTP</p></blockquote>
<p>不寫來源跟章節定位、findings 會變成抽象列表、寫稿時用不上。</p>
<h3 id="case-類型的承接策略">Case 類型的承接策略</h3>
<p>不同 case 類型適合不同承接深度、誤判類型會引發 <em>over-extrapolation</em> 問題。實作中觀察到的兩類 case：</p>
<p><strong>Rich case</strong>（典型：09/07 案例庫中含具體數字、設計細節、遷移路徑的長篇 case）：</p>
<ul>
<li>內容深度：50-200 行、含具體數字、業務情境、引用源</li>
<li>承接方式：可直接引用為事實、case 揭露的具體數字（RPS、延遲、TPS、stale window）可放進章節</li>
<li>例：9.C5 Amazon Ads「90M RPS + 5M writes/sec + 99.999%」可直接寫進 1.10 KV 章節</li>
<li>例：9.C6 Tinder「4700 萬 MAU 配對引擎、cache 是主要服務面」可直接做為 2.1 high-concurrency 的判讀依據</li>
</ul>
<p><strong>Medium case</strong>（06 模組新發現的類別、典型：模組內部 case 庫中含結構化「決策機制」+「可觀測訊號」表、但無具體數字的中篇 case）：</p>
<ul>
<li>內容深度：30-50 行、結構化 5 段（問題場景 / 決策機制 / 可觀測訊號 / 常見陷阱 / 下一步路由）、含 mechanism + 訊號名稱、但不給具體數字</li>
<li>承接方式：用 case 直接列出的 <em>mechanism 名稱</em> 精準引用、比 skeleton 精準、但比 rich 保守</li>
<li>承接句型：「對應 [case]：揭露 N 個機制 — A、B、C、D」</li>
<li>例：6.C1 Amazon Shuffle Sharding 揭露 cell boundary / shuffle sharding / static stability / constant work 四機制、可直接引用機制名稱、但不擴寫到「具體 shard 數量」「具體 cell 大小」等 case 沒提的實作細節</li>
</ul>
<p><strong>Skeleton case</strong>（典型：模組內部 N.Cx 案例庫中只有 frame、無具體數字的短篇 case）：</p>
<ul>
<li>內容深度：10-30 行、只給方向、無具體數字 / taxonomy</li>
<li>承接方式：作為「視角 / 方向」、可引用為「case 揭露 X 議題」、但不引用為「case 揭露 X 具體場景數量」</li>
<li>例：2.C1 Meta Cache Consistency 只有「promotion、shard move、故障恢復」三個方向、不引用為「具體 inconsistency window 數字」</li>
<li>例：3.C9 反例只給「依賴特定 offset / 重試節奏 / idempotency」三個方向、不引用為「4 個具體誤配場景」</li>
</ul>
<p><strong>判讀條件</strong>：</p>
<ul>
<li>看 case 行數 + 內容密度判斷類型</li>
<li>skeleton case 的 finding 寫成「對應 [case] — 揭露 X 方向、以下展開基於通用工程知識補充」</li>
<li>medium case 的 finding 寫成「對應 [case]：揭露 N 個機制 — A、B、C、D」、用 mechanism 名稱精準引用</li>
<li>rich case 的 finding 可寫「對應 [case] — XXX 具體數字 / 設計」</li>
</ul>
<p>實作中（01/02/03 三個模組驗證）、skeleton case 寫成 rich case 對應是 case fidelity reviewer 抓出 over-extrapolation 的主要來源（02 / 03 各 3-4 個 critical 編造都來自此陷阱）。誤判類型 → 編造 case 沒寫的細節 → reviewer 抓出 → 修正成本高。stage 1 抽 findings 時就要 <em>標明 case 類型</em>、stage 2 寫作時依類型決定承接深度。</p>
<p><strong>Rich case 引用的反向風險（04/05 模組新發現）</strong>：rich case 雖然可以引用具體數字、但 case 內常含「觀察層」（具體 fact）跟「判讀層」（作者推論）兩段、引用時要分開處理。05 模組驗證時 case fidelity reviewer 抓出 4 個 high issue 都來自把「判讀層作者推論」寫成「case 揭露的 fact」：</p>
<ul>
<li>9.C12 Riot Games：5.2 寫「揭露 35ms latency 反推 region 部署」、實際 case 的「35ms」是觀察層、「反推 region 部署」是作者判讀層</li>
<li>9.C34 GCP 130K：5.2 寫「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」、實際 case 用更保守的「control plane 極限取決於 storage backend、GCP 用 Spanner 替換 etcd」分兩個點寫</li>
<li>9.C12 Riot：5.2 引用「single-tenant per game 的多 cluster 策略」、漏掉 case 揭露的關鍵歷史轉折「從 multi-tenant cluster 模型改成 single-tenant per game」</li>
</ul>
<p><strong>修法</strong>：rich case 引用時、用「揭露 X 觀察 + 作者判讀 Y」分層標明、避免把推論寫成 fact。或在引用後補一句「（case 中 X 屬作者判讀層、本章引用此推論）」明示分層。</p>
<p>兩類 case 的引用紀律可總結成一個 <em>fact vs derive</em> 分層原則：</p>
<ul>
<li><strong>Skeleton case</strong>：絕大多數內容是 derive（方向 / 議題）、引用時不擴寫成 fact</li>
<li><strong>Rich case</strong>：含 fact（具體數字 / 設計）跟 derive（作者判讀）、引用時分層標明、避免把 derive 升級成 fact</li>
</ul>
<h2 id="階段-2基於-findings-建立內容">階段 2：基於 findings 建立內容</h2>
<h3 id="findings-分布到章節">Findings 分布到章節</h3>
<p>抽完 findings 後、按章節主題分類、看哪個章節缺口最大、哪個 finding 該寫去哪。實作中的分布：</p>
<ul>
<li>1.1 高併發：7 findings</li>
<li>1.5 紅隊：8 findings</li>
<li>1.9 reconciliation：4 findings</li>
<li>1.10 KV：6 findings</li>
<li>1.11 全球分散式：10 findings（最大缺口）</li>
<li>1.6+1.12 migration：5 findings</li>
</ul>
<p>涉及多軸取捨的章節（1.11 一致性 / 可用性 / 成本 / 延遲）暴露最多缺口、純流程章節（1.9）暴露最少。這是 <em>章節結構性質</em> 的差異、不是寫得好壞。</p>
<h3 id="stage-2-寫作前先定-ssot-對應">Stage 2 寫作前先定 SSoT 對應</h3>
<p>當同一 finding 或 frame 在 <em>多個章節</em> 都有用、要在開始寫之前 <em>先定 SSoT 對應</em>、否則 case-driven 擴章必然出現 frame 重複展開。</p>
<p>實作中觀察到的反例（02 / 03 模組都遇到過）：</p>
<ul>
<li><strong>02 cache</strong>：「cache 角色變化」frame 在 2.1 主寫但實際屬模組層級、應在 <code>_index</code>；Tubi 案例在 2.1 / 2.2 / 2.8 三章各自展開 mini-finding；Snap KeyDB 在 2.1 / 2.7 / 2.8 三章重複</li>
<li><strong>03 message-queue</strong>（最嚴重）：「三層語意（delivery / processing / recovery）」在 3.4 / 3.6 / 3.8 三章各自定義；「Slack Kafka+Redis 拓樸」在 3.4 跟 3.8 兩章逐字重複；「規模對照（小 / 中 / 大型）」在 3.4 / 3.6 / 3.8 三章拆用、結論散落讀者拼不出總圖</li>
</ul>
<p><strong>SSoT 對應的判讀順序</strong>：</p>
<ol>
<li>列出所有 cross-chapter findings（出現在多章的 frame）</li>
<li>每個 frame 指定 <em>一個</em> 主寫章節（SSoT）</li>
<li>其他章節 <em>只 link</em>、不展開</li>
<li>SSoT 章節要有完整論述、被引用章節保留簡述跟 cross-link</li>
</ol>
<p><strong>SSoT 選擇標準</strong>：</p>
<ul>
<li>frame 涉及 <em>跨模組層級概念</em> → 寫進 <code>_index.md</code></li>
<li>frame 涉及 <em>單章核心責任</em> → SSoT 為該章</li>
<li>frame 涉及 <em>跨章交接點</em> → 選最相關章節為 SSoT、其他章節 link</li>
</ul>
<p>漏掉這步、reviewer 跨章一致性會抓出 5-10 個 frame 重複 issue、修正成本高（要把已展開內容收斂回 SSoT）。Stage 2 前花 30 分鐘做 SSoT 對應、能省下 Stage 3 數小時的重構工。</p>
<h3 id="避免硬塞模板">避免硬塞模板</h3>
<p>最大的反模式是把多個 findings 硬塞成同一個 table、每 row 一短語、失去情境敘事。</p>
<p>實作中的反例：1.9 章新增「Dual-track IC 5 個角色表」、本來想用表格整齊呈現、但 reviewer 抓出「5 角色平鋪、責任只一行、未展開每角色在真實事故的決策樣態」。修正後拆成：</p>
<ul>
<li>主表格（5 個角色快速對照）</li>
<li>Overall IC 跟 Tech IC 的差異獨立段（300 字）</li>
<li>Data IC 的特殊角色獨立段（300 字、含「為什麼不能讓 Tech IC 兼任」的失誤對照）</li>
<li>事先準備 4 項各自延伸（不只列項目、解釋失效樣態）</li>
</ul>
<p>這樣 <em>每個項目都是情境</em> 而非 <em>硬塞的欄位</em>、符合 AGENTS.md「表格不是終點」原則。</p>
<h3 id="情境敘事的判讀條件">情境敘事的判讀條件</h3>
<p>每段內容寫完後、問三個檢查問題：</p>
<ol>
<li><strong>首句是不是核心原則</strong>？（不是「某 case 揭露 X」、是「X 是什麼、承擔什麼責任」）</li>
<li><strong>是不是用否定句主導</strong>？（「不是 X」「不只 X」開段要回到正向陳述）</li>
<li><strong>這個 finding 在不同情境下是否會變義</strong>？（一個 finding 套到多個情境、要分情境寫、不是套同模板）</li>
</ol>
<h3 id="案例引用的準確性">案例引用的準確性</h3>
<p>寫「對應 [case] — XXX」時、要回 case 原文驗證 XXX 是否真的出現。實作中常見的失分：</p>
<ul>
<li>把 case 沒提到的數字補進去（「30-90 天 baseline」、「1MB→10GB / 天」）</li>
<li>把通用 best practice 寫成案例事實（「Snowflake 之後改為預設強制 MFA」— case 只說「資料平台應預設強制 MFA」、不是描述後續行動）</li>
<li>公開事實但 case 沒寫（「MOVEit 跨上百家客戶」、「LastPass master password 弱可被離線爆破」）</li>
</ul>
<p>寫稿當下不容易抓、要靠階段 3 的 case fidelity reviewer 對照。</p>
<h2 id="階段-3agent-team-平行多輪審查">階段 3：Agent team 平行多輪審查</h2>
<h3 id="為什麼要-agent-team不能交給單一-reviewer">為什麼要 agent team、不能交給單一 reviewer</h3>
<p>單一 reviewer 有兩個限制：</p>
<ol>
<li><strong>維度盲點</strong>：一個 reviewer 同時看寫作規範、案例準確性、跨章一致性、容易 <em>維度互相干擾</em>、最後每個維度都看不深</li>
<li><strong>Context 污染</strong>：reviewer 讀完整 commit + 所有案例 + 所有章節後、自身 context 就被佔滿、給的建議會 <em>對應主 context 也跟著沉重</em></li>
</ol>
<p>解法是用 3 個專責 reviewer、平行 background 跑、各自獨立報告、主 context 只看精煉摘要。</p>
<h3 id="三個維度-reviewer-分工">三個維度 reviewer 分工</h3>
<p>實作中使用的三個 reviewer：</p>
<h4 id="reviewer-a寫作規範審查agentsmd-核心原則">Reviewer A：寫作規範審查（AGENTS.md 核心原則）</h4>
<ul>
<li>對照核心原則先行、正向陳述優先、商業邏輯先於 case、表格不是終點、情境優先於模板、可操作判準等八原則</li>
<li>找首句用否定句切入、表格 / bullet 平鋪沒延伸、表格項硬塞模板等</li>
<li>實作中抓出 25 個 issue</li>
</ul>
<h4 id="reviewer-b案例引用準確性">Reviewer B：案例引用準確性</h4>
<ul>
<li>對照原始 case 內容、驗證「對應 [case] — XXX」斷言是否真的來自案例</li>
<li>識別編造數字、過度推論、把通用 best practice 寫成案例事實</li>
<li>實作中抓出 9 個 issue、包含 3 個 critical 編造</li>
</ul>
<h4 id="reviewer-c跨章一致性">Reviewer C：跨章一致性</h4>
<ul>
<li>跨多章找重複 frame、矛盾說法、失效 cross-link、章節邊界錯位</li>
<li>識別「該在 A 章卻寫在 B 章」、「frame 重複展開沒整併」</li>
<li>實作中抓出 13 個 issue</li>
</ul>
<h3 id="平行-background-跑不佔主-context">平行 background 跑、不佔主 context</h3>
<p>關鍵設計是 3 個 reviewer 並行、各自 background、各自寫 output file、不污染主 context：</p>
<ul>
<li>主 context 只看到「啟動 reviewer」跟「reviewer 完成的彙整報告」</li>
<li>Raw output 跟 reviewer 的 deep dive 留在 output file、需要時 SendMessage 繼續對話</li>
<li>3 個 reviewer 完成時間 ~5-15 分鐘、可以同時跑、不必等</li>
</ul>
<p>實作中 3 個 reviewer 平均 2-3 分鐘完成、主 context 增量 ~3K tokens（彙整 + 47 issue 清單）、相比把所有案例跟章節塞進主 context 做 review 節省 ~80% context。</p>
<h3 id="reviewer-issue-數量的-baseline">Reviewer issue 數量的 baseline</h3>
<p>7 個模組（01 / 02 / 03 / 04 / 05 / 06 / 07 batch 1）驗證後、每模組 reviewer 抓到的 issue 數量在 standards reviewer 抓 pattern 越來越細的趨勢下持續擴大、可作為流程預期：</p>
<table>
  <thead>
      <tr>
          <th>Reviewer 維度</th>
          <th>01</th>
          <th>02</th>
          <th>03</th>
          <th>04</th>
          <th>05</th>
          <th>06</th>
          <th>07 b1</th>
          <th>baseline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Standards reviewer</td>
          <td>25</td>
          <td>20</td>
          <td>20</td>
          <td>31</td>
          <td>28</td>
          <td>45</td>
          <td>31</td>
          <td>20-45 issue</td>
      </tr>
      <tr>
          <td>Case fidelity reviewer</td>
          <td>9 (88%)</td>
          <td>20 (78%)</td>
          <td>15 (70%)</td>
          <td>6 (92.9%)</td>
          <td>13 (80%)</td>
          <td>11 (88%)</td>
          <td>8 (81%)</td>
          <td>6-20 issue</td>
      </tr>
      <tr>
          <td>Consistency reviewer</td>
          <td>13</td>
          <td>15</td>
          <td>15</td>
          <td>14</td>
          <td>18</td>
          <td>15</td>
          <td>13</td>
          <td>13-18 issue</td>
      </tr>
      <tr>
          <td><strong>總計</strong></td>
          <td><strong>47</strong></td>
          <td><strong>55</strong></td>
          <td><strong>50</strong></td>
          <td><strong>51</strong></td>
          <td><strong>59</strong></td>
          <td><strong>71</strong></td>
          <td><strong>52</strong></td>
          <td><strong>47-71 issue</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>模式觀察</strong>：</p>
<ul>
<li><strong>每模組 issue 數隨 standards reviewer 抓 pattern 越來越細而擴大</strong>：01-03 穩定在 47-55、04/05 推到 51-59、06 推到 71、07 batch 1 回到 52（章節已有 routing skeleton、擴章規模小）。趨勢來自 standards reviewer 抓的 pattern 越來越廣（從負向骨架 → 「核心責任不是」變體 → 「沒有 X 會 Y」鏈式 → 「case 引用段首」框架 → 「case 引用句構同質化」）。</li>
<li><strong>Case fidelity 準確率分布更廣</strong>：04 的 92.9% 來自 skeleton case 嚴守「揭露方向、通用補充」紀律；05 的 80% 因引用 09 rich case 加入「fact vs derive 分層」新失分模式；06 的 88% 屬 medium case 紀律首次套用、揭露「實作層擴寫過頭」失分；07 batch 1 的 81% 揭露「跨 case 合成 frame」新失分類型（reviewer B 2 high 都屬此類）</li>
<li><strong>Consistency reviewer 抓到的 frame 重複跟章節數成正比</strong>：02 / 03 / 04 都有 ~13-18 個一致性 issue、05/06 跨模組 cross-link 密度高仍維持在 baseline 內、07 batch 1 因 7 章規模、issue 13 個落在 baseline 下緣</li>
</ul>
<p><strong>Stage 3 修正成本估算</strong>：</p>
<ul>
<li>Critical（編造、矛盾）：~每個 5-10 分鐘修正、佔 0-5 個（04/05 都 0 critical、紀律已成熟）</li>
<li>High（重複 frame、章節邊界、判讀層 vs fact）：~每個 10-20 分鐘修正、佔 5-14 個</li>
<li>Medium / Low（規範細節、cross-link 補）：~每個 2-5 分鐘修正、佔 35-45 個</li>
<li><strong>總計 ~1.5-2.5 小時 / 模組</strong></li>
</ul>
<p><strong>Stage 4 修正後仍會有 ~30-40% issue 殘留</strong>（low / medium 的 cross-link、編號漂移、用語不一）、屬於系統性 pattern、適合在 Stage 5 polish pass 集中處理（見後段）。</p>
<h3 id="為何要多輪-review不是一次到位">為何要多輪 review、不是一次到位</h3>
<p>第一輪 review 的目的是 <em>找問題</em>、不是 <em>修問題</em>。問題清單列出後、要做兩件事：</p>
<ol>
<li><strong>分類優先序</strong>：critical / high / medium / low、按嚴重度跟修改成本排序</li>
<li><strong>修正循環</strong>：批次修正、避免一個一個改散開、修完再跑驗證</li>
</ol>
<p>修正後可選擇性做第二輪 review、檢查：</p>
<ul>
<li>修正本身有沒有引入新問題</li>
<li>之前 reviewer 漏掉的維度（例：教學性、讀者路徑、實作可行性）</li>
<li>跨 commit 一致性</li>
</ul>
<p>實作中第一輪足夠處理 47 個 issue、第二輪沒進行、留到未來模組（02 cache、03 message queue）累積經驗後再評估是否必要。</p>
<h2 id="修正循環的執行原則">修正循環的執行原則</h2>
<p>47 個 issue 分布到 6 個章節、修正時 <em>按檔案批次</em>、不是按 issue 編號順序。每個檔案一次修完所有相關 issue、減少切換成本：</p>
<ul>
<li>1.5 紅隊章（12 issue）：含 2 個 critical 編造、優先處理</li>
<li>1.10 KV（7 issue）：含 1 個 critical 編造</li>
<li>1.11 全球分散式（5 issue）</li>
<li>1.12 大規模遷移（10 issue）：表格密度最高、最多延伸</li>
<li>1.1 高併發（4 issue）</li>
<li>1.9 reconciliation（5 issue）</li>
</ul>
<p>每個檔案修完後跑一次 <code>mdtools fmt --fix</code> + <code>mdtools cards</code> + <code>mdtools lint</code>、確認該檔內部一致、再進下一檔。最後跑一次跨檔驗證、確認 cross-link 全部對齊。</p>
<h2 id="階段-5polish-pass0405-模組後新增">階段 5：Polish pass（04/05 模組後新增）</h2>
<p>Stage 4 修完 high + 重要 medium 後、仍有 ~30-40% 的 low / medium 殘留、屬於系統性 pattern（負向骨架、編號漂移、cross-link 缺漏、模板化）。這些 issue 不適合按章節批次修、適合用「跨檔系統性掃描」處理 — 這是 polish pass 的核心責任。</p>
<h3 id="polish-pass-的觸發條件">Polish pass 的觸發條件</h3>
<p>Stage 4 後出現以下任一訊號、就該排 polish pass：</p>
<ul>
<li>Standards reviewer 抓出的「不是 X、而是 Y」段首結構超過 5 處（屬寫作習慣、單章修改無效率）</li>
<li>Consistency reviewer 抓出「編號漂移」「失效 link」「用語不一」多處（屬跨檔規範問題）</li>
<li>自掃描漏掉的 pattern 出現在 reviewer report（例：04 自掃描說 pass、reviewer A 抓出 31 個 issue、暴露自掃描 regex 不夠寬）</li>
</ul>
<h3 id="polish-pass-不該做的事">Polish pass 不該做的事</h3>
<ul>
<li><strong>不重寫章節結構</strong>：polish pass 是把現有內容修得更貼合規範、不是重新組織。重寫的觸發條件應該回到 stage 2、不是 polish pass。</li>
<li><strong>不擴大 scope</strong>：原本 4.20 / 5.4 等不在擴充範圍的章節、polish pass 也不動。Polish pass 邊界 = stage 4 修改過的章節集合。</li>
<li><strong>不追求 0 issue</strong>：reviewer 抓的 ~15 個 low 通常可保留為下次擴章節時自然處理。Polish pass 處理「系統性 pattern」、不處理「孤立 issue」。</li>
</ul>
<h3 id="polish-pass-的標準工序">Polish pass 的標準工序</h3>
<p>按系統性 pattern 分批處理、每批跑一次自掃描確認：</p>
<ol>
<li><strong>負向骨架掃描修正</strong>：用更寬泛的 regex <code>不是 |而不是|沒有.*[，、]會</code> 掃描、把「不是 X、而是 Y」「而不是 X」改成正向陳述 + 後置邊界提醒。技術約束敘述（「多人共用 IP 無法區分」）保留。</li>
<li><strong>編號漂移統一</strong>：把 <code>04.X</code> 風格 plain text 改成 <code>[4.X title](url)</code> markdown link、跟 _index 對齊。</li>
<li><strong>表格延伸段補強（關鍵段）</strong>：選 2-3 個最高 impact 表格（判讀訊號表的爭議列、Buffer / Sampling 等選型表）補延伸子段、不全部補（避免擴展超出 scope）。</li>
<li><strong>模板化拆敘事（代表性段）</strong>：選 1-2 個最明顯的「四步驟模板套不同情境」段、拆成情境化敘事、其他保留為下次。</li>
<li><strong>Cross-link 補漏 + ownership 邊界補強</strong>：reviewer C 報告的所有 cross-link 缺漏一次補完、用同一個批次跑 mdtools 驗證。</li>
<li><strong>用語不一統一 + 失效 link 修正</strong>：簡轉繁、<code>/knowledge-cards/</code> vs <code>/section/</code> URL 統一、失效 link 改規劃中或正確路徑。</li>
<li><strong>最終驗證 + commit</strong>：跑 <code>mdtools fmt --fix &amp;&amp; mdtools cards &amp;&amp; mdtools lint</code>、確認全綠、commit。</li>
</ol>
<h3 id="polish-pass-的實作成本">Polish pass 的實作成本</h3>
<p>實作中（04 / 05 polish pass 合併 commit <code>1072087</code>）：</p>
<ul>
<li>處理範圍：11 個檔案、+44 / -29 行</li>
<li>修正項目：~35 個 issue（10 個負向骨架、2 個模板化、3 個編號漂移、3 個表格延伸段、3 個 cross-link、1 個 case 引用結構）</li>
<li>時間：~30-45 分鐘（不重寫、只 pattern match）</li>
<li>剩餘 ~15 個 low 保留下次</li>
</ul>
<p>Polish pass 的 ROI 來自「系統性 pattern 一次處理 vs 散在各章一個個改」的效率差異。每個 pattern 在多章重複出現時、用 grep / rg 跨檔修一輪比每章單獨修快 3-5 倍。</p>
<h3 id="自掃描盲點更新">自掃描盲點更新</h3>
<p>04 流程暴露了一個 self-scan 盲點：原 regex <code>不行|不可以|不要|無法|不能</code> 漏掉「核心責任不是 X、而是 Y」這個變體段首。修正建議：</p>
<ul>
<li>加 <code>^[^|].*責任(不是|並非)</code> 抓「核心責任不是 X」變體</li>
<li>加 <code>^[^|].*[，,]而是</code> 抓「X、而是 Y」結構（已是正常陳述、但段首位置仍是負向骨架）</li>
<li>加 <code>^[^|].*[，,]不是</code> 抓「X、不是 Y」結構</li>
</ul>
<p>把自掃描 regex 視為持續演進的工具、每個 reviewer 抓出新 pattern 就更新一次、避免在下個模組重蹈覆轍。</p>
<h2 id="適用情境跟限制">適用情境跟限制</h2>
<h3 id="適用情境">適用情境</h3>
<ul>
<li><strong>長期累積的教學模組</strong>：6+ 章、跨章引用密集、規範遵循重要</li>
<li><strong>有現成 case 庫</strong>：07/09 累積的 100+ 案例是這套流程的前提、沒案例庫做不到 case-first</li>
<li><strong>品質高於速度</strong>：完整三階段約 3-4 小時 / 模組（stage 2 寫作 ~1.5-2hr + reviewer ~15 分鐘 + stage 3 修正 ~1.5-2hr）、適合長期累積的內容、不適合 one-off 文章</li>
<li><strong>主 context 容量敏感</strong>：reviewer 平行 background 是節省 context 的關鍵設計</li>
</ul>
<h3 id="不適用情境">不適用情境</h3>
<ul>
<li><strong>新主題沒案例庫</strong>：要先建案例庫、不能直接套這流程</li>
<li><strong>單篇短文</strong>：流程的固定成本（讀案例 + 跑 reviewer）對短文 ROI 低</li>
<li><strong>快速迭代原型</strong>：流程偏向 <em>寫一次寫好</em>、不是 <em>快速修改</em></li>
<li><strong>Routing layer / 導讀性質章節</strong>：已含完整 threat scope + 引用標準 + 問題節點表、case 庫不對應或缺位、應跳過本流程、用標準引用 + 通用工程知識補充承接（07 LLM / 治理章節驗證）</li>
<li><strong>Standard framework 比 case 庫成熟的領域</strong>：見下段「Standard-driven 取代 case-driven」</li>
</ul>
<h3 id="standard-driven-取代-case-driven07-llm-章節驗證">Standard-driven 取代 case-driven（07 LLM 章節驗證）</h3>
<p>在標準框架比 case 庫成熟的領域、case-driven 不是預設選擇。LLM 安全章節跑完 5 章驗證後浮現一個 finding：當該領域的 <em>標準框架</em>（如 OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS）已涵蓋 threat 分類、且 case 維護半衰期短於 standard、章節應 <em>用 standard-driven 取代 case-driven</em>。Standard-driven 跟 case-driven 是平行選項、依領域特性選用 — 兩者沒有退化 / 進階關係。</p>
<p><strong>判斷該用哪種策略的四維度</strong>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Case-driven 適用</th>
          <th>Standard-driven 適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>議題穩定度</td>
          <td>高（5+ 年穩定）</td>
          <td>低（&lt; 1 年快速演進）</td>
      </tr>
      <tr>
          <td>Case 公開度</td>
          <td>高（充分的事故公告）</td>
          <td>中或低（vendor disclosure 偏 marketing）</td>
      </tr>
      <tr>
          <td>Standard 成熟度</td>
          <td>中（多用 case 而非 standard）</td>
          <td>高（standard framework 已成型）</td>
      </tr>
      <tr>
          <td>維護半衰期</td>
          <td>長</td>
          <td>短（6 個月過時）</td>
      </tr>
  </tbody>
</table>
<p><strong>典型對照</strong>：</p>
<ul>
<li><em>Case-driven 領域</em>：分散式系統 / 安全控制面 / 可靠性 / 訊息佇列（backend/01-07 batch 1 都屬此類、案例公開充分、半衰期 5+ 年）</li>
<li><em>Standard-driven 領域</em>：LLM 安全（OWASP LLM Top 10 / MITRE ATLAS 已成型、案例 6 個月過時）、新興 compliance（NIST AI RMF）、cloud-native 標準（CNCF baseline）</li>
</ul>
<p><strong>Standard-driven 章節的寫作策略</strong>：</p>
<ol>
<li><strong>章節對齊 standard framework 分類</strong>：用 framework 章節 ID 標明（如 OWASP LLM01 / NIST AI-1.1）取代「對應 [case] —」斷言</li>
<li><strong>加 Last reviewed cadence</strong>：每 quarter 重評估 standard 版本跟章節對應、寫進 frontmatter</li>
<li><strong>「案例觸發參考」段標明「公開案例累積中、值得追蹤的方向」</strong>：不寫「對應 [case] 揭露」斷言、避免引用源不穩定</li>
<li><strong>引用標準時用版本號</strong>：OWASP LLM Top 10 2025 / NIST AI RMF 1.0 / MITRE ATLAS continuous — framework 改版要 trigger 章節重審</li>
</ol>
<p><strong>實證</strong>：07 LLM 章節 5 章已套用 standard-driven 策略：</p>
<ul>
<li>章節 113-137 行、含完整 threat scope + 問題節點表 + 風險邊界</li>
<li>引用 OWASP LLM Top 10 + NIST AI RMF + MITRE ATLAS 取代個別 case 引用</li>
<li>加 <code>Last reviewed: 2026-05-12</code> cadence</li>
<li>「案例觸發參考」段寫「公開案例累積中、值得追蹤的方向」+「事實查核註」</li>
<li>完全不寫「對應 [case] —」斷言、不存在 case fidelity reviewer 該抓的準確性問題</li>
</ul>
<p>對照 backend/01-07 batch 1 的 case-driven 章節、LLM 章節是 <em>用不同方法達到同樣品質</em> — scope 涵蓋真實 production 議題（KV cache 跨租戶、shared prefix optimization、batch 推論順序敏感）、不停在教科書級內容。</p>
<p><strong>何時要從 standard-driven 轉回 case-driven</strong>：</p>
<ul>
<li>該領域累積 5+ 個高可信度 case（vendor disclosure + academic paper + CVE 三來源交叉）</li>
<li>跨章 frame 重複出現、case-driven mechanism 深化能解 SSoT 衝突</li>
<li>出現「等級類似 SolarWinds」的 incident、案例本身夠重、單一 case 即可支撐章節擴章</li>
<li>讀者反饋章節太抽象、需要具體 case 才能理解 mechanism</li>
</ul>
<p>不滿足任一條件時、繼續走 standard-driven、不勉強建 case 庫。</p>
<p><strong>對 case-first-module-workflow skill 的補強</strong>：</p>
<p>skill 之前的「不適用情境」寫「沒 case 庫的新主題（要先建 case 庫）」— 這暗示缺 case 庫一定要先補。07 LLM 章節驗證了第三條路：<em>用 standard-driven 取代</em>、適用 standard framework 比 case 庫成熟的領域。這個 finding 已補進 skill 的「不適用情境」段。</p>
<h3 id="限制">限制</h3>
<ul>
<li><strong>Reviewer 維度有限</strong>：當前 3 個 reviewer 沒覆蓋「教學性」「讀者路徑」「實作可行性」、若主題需要這些維度、要加 reviewer</li>
<li><strong>修正可能引入新 issue</strong>：第一輪 review 後修正、修正本身可能違反規範、若大量修正最好做第二輪</li>
<li><strong>Case 庫品質決定 findings 品質</strong>：case 寫得淺、findings 也淺；case fidelity reviewer 也只能驗證「跟 case 一致」、不能驗證「case 本身對不對」</li>
<li><strong>依賴 LLM agent 平台能力</strong>：流程預設可平行跑 background agent、不是所有 LLM 平台都支援</li>
</ul>
<h2 id="7-個模組驗證後的反覆陷阱">7 個模組驗證後的反覆陷阱</h2>
<p>01 / 02 / 03 / 04 / 05 / 06 / 07 七個模組執行下來、以下陷阱在 <em>多數模組都重複出現</em>、屬於 LLM case-driven 寫作的系統性失分點。本流程下次套用前要 <em>主動防範</em>、不能依賴 stage 3 reviewer 補救（雖然 reviewer 都會抓到、但修正成本高）。</p>
<h3 id="陷阱-1skeleton-case-擴寫成-case-事實">陷阱 1：Skeleton case 擴寫成 case 事實</h3>
<p>當 case 內容簡短（10-30 行、只有 frame 沒有具體數字 / taxonomy）、LLM 寫作時容易把通用知識（具體數字、攻擊向量列表、設計細節）寫成「對應 [case] —」斷言。實際 case 沒寫的。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01 紅隊：Snowflake「30-90 天 baseline」編造、Tixcraft「partition key 用 user_id」編造</li>
<li>02 cache：Tubi 三層 cache 具體 latency（L1 &lt; 1ms、L2 &lt; 10ms、L3 10-100ms）編造、Redis「100K-200K ops/sec」無來源、KeyDB「5-10x throughput」其實是 case 判讀段非引用源</li>
<li>03 messaging：PayPay「broker 寫入 3K msg/sec」實際 case 寫的是「DynamoDB 寫入 3K msg/sec」（PayPay 用 DynamoDB 不是傳統 broker）、3.C9 case 三個方向被擴寫成「4 個誤配場景」、3.C10 case 「大型服務 DLQ 是診斷入口」完全編造</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 1 抽 findings 時 <em>標明 case 類型</em>（rich vs skeleton）</li>
<li>Stage 2 寫 skeleton case finding 時、用「對應 [case] — 揭露 X 方向、以下展開基於通用工程知識補充」這種 <em>fact vs derive</em> 標記</li>
<li>不要為了「整齊的 4 個攻擊面」「3 個攻擊向量」「5 個誤配場景」這種數字感、把 case 沒寫的 taxonomy 寫成 case 揭露</li>
</ul>
<h3 id="陷阱-2frame-重複展開ssot-不清">陷阱 2：Frame 重複展開（SSoT 不清）</h3>
<p>同一概念在多章 case-driven 擴章時各自展開、形成 frame 重複。讀者跨章讀會踩到重述、結論散落拼不出總圖。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01：容量三口徑 frame 在 1.1 跟 1.12 重複展開、storage / compute 分離 frame 在 1.1 跟 1.11 重複</li>
<li>02：cache 角色變化 frame 在 2.1 主寫但屬模組層級、應在 _index；Tubi 案例在 2.1 / 2.2 / 2.8 三章 mini-展開</li>
<li>03（最嚴重）：三層語意（delivery / processing / recovery）在 3.4 / 3.6 / 3.8 三章各自定義；Slack Kafka+Redis 拓樸在 3.4 跟 3.8 兩章逐字重複；規模對照在 3.4 / 3.6 / 3.8 三章拆用</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 2 寫作前花 30 分鐘做 SSoT 對應（見前面「Stage 2 寫作前先定 SSoT 對應」段）</li>
<li>列出 cross-chapter frames、指定唯一主寫章節、其他章節只 link</li>
<li>寫每章前問「這個 frame 主寫在哪？我現在寫的是主寫還是 link？」</li>
</ul>
<h3 id="陷阱-3負向陳述--模板化規範系統性失分">陷阱 3：負向陳述 + 模板化（規範系統性失分）</h3>
<p>「不是 X、是 Y」推進論證、L1/L2/L3 三層平鋪、三選一表格、四步驟流程。這兩個原則違反在每模組都重複出現、是 LLM 寫作的反覆模式、stage 3 standards reviewer 每模組會抓 10-20 處。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>01 規範 violation：表格不延伸（7 處）、負向陳述（5 處）、首句結構（4 處）</li>
<li>02 規範 violation：原則 8 模板化（6 處）、原則 2 負向陳述（6 處）、原則 4 表格不延伸（4 處）</li>
<li>03 規範 violation：原則 2 負向陳述（12 處最嚴重）、原則 1 首句結構（5 處）、原則 6 用語節制（2 處）</li>
<li>04 規範 violation：原則 2 負向陳述（12 處最嚴重、含「核心責任不是 X、而是 Y」變體段首）、原則 1 首句結構（9 處）、原則 4 表格不延伸（9 處）</li>
<li>05 規範 violation：原則 2「不是 X、而是 Y」+「沒有 X、會 Y」（10 處）、原則 8 四步驟 / 四層並列模板（7 處）、原則 3 case 引用框架取代商業邏輯先行（6 處）</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Stage 2 寫完後 <em>寫稿端就跑掃描</em>、不等 reviewer：
<ul>
<li><code>rg -n &quot;不行|不可以|不要|無法|不能&quot; &lt;module-path&gt;</code> 找負向骨架（技術約束敘述例外）</li>
<li><code>rg -n &quot;^[^|].*責任(不是|並非)&quot; &lt;module-path&gt;</code> 找「核心責任不是 X」變體段首（04 模組新發現的 pattern）</li>
<li><code>rg -n &quot;^[^|].*[，,]而是|^[^|].*[，,]不是&quot; &lt;module-path&gt;</code> 找對比骨架開段</li>
<li>自查表格：每個 bullet 是否有後文延伸？</li>
<li>自查首句：是否「核心原則先行」而非「對應 [case] 揭露」</li>
</ul>
</li>
<li>模板化（L1/L2/L3、三選一）出現時、先問「這三項是真的對等？還是業務情境不同？」— 不同情境的話拆敘事段、不用表格</li>
</ul>
<h3 id="陷阱-4rich-case-判讀層被當-case-fact-引用0405-模組新發現">陷阱 4：Rich case 判讀層被當 case fact 引用（04/05 模組新發現）</h3>
<p>引用 09 / 07 等 rich case 時、case 內常含「觀察層」（具體 fact）跟「判讀層」（作者推論）兩段。LLM 寫作時容易把兩層壓縮成「揭露 X」、把作者判讀升級為 case fact。</p>
<p>跟陷阱 1（skeleton case 擴寫成 case 事實）的差別：</p>
<ul>
<li><strong>陷阱 1</strong>：case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露</li>
<li><strong>陷阱 4</strong>：case 有提、但屬作者判讀層的內容被寫成 case fact</li>
</ul>
<p><strong>實證</strong>：</p>
<ul>
<li>05 / 9.C12 Riot：5.2 寫「揭露 35ms latency 反推 region 部署」、實際 case 的「35ms」是觀察層、「反推 region 部署」是作者判讀層</li>
<li>05 / 9.C34 GCP：5.2 寫「揭露 Spanner 替 etcd 才是 K8s 規模極限的關鍵」、實際 case 用更保守的「control plane 極限取決於 storage backend、GCP 用 Spanner 替換 etcd」分兩個點寫、章節壓縮 + 強化成硬性結論</li>
<li>05 / 9.C12 Riot：漏掉 case 揭露的關鍵歷史轉折「從 multi-tenant cluster 模型改成 single-tenant per game」</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>引用 rich case 前、先把 case 內的「觀察段」跟「判讀段」分開讀、抽 finding 時各自標明來源層</li>
<li>引用時用「揭露 X 觀察 + 作者判讀 Y」分層寫、或在引用後補一句「（case 中 X 屬作者判讀層、本章引用此推論）」</li>
<li>避免使用「才是 / 必須 / 一定」這類強化詞、保留 case 原文的條件性表述</li>
<li>Stage 3 case fidelity reviewer 的 prompt 要特別點出「判讀層 vs 觀察層」的分界、把這當作 high 級 issue 抓取</li>
</ul>
<h3 id="陷阱-5自掃描盲點累積040506-模組持續顯現">陷阱 5：自掃描盲點累積（04/05/06 模組持續顯現）</h3>
<p>自掃描的 regex 跟 reviewer 抓的 pattern 會逐漸脫節。每個模組 reviewer 會發現新 pattern、self-scan regex 跟著演進、但 reviewer 仍會發現下一個。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>04 自掃描用 <code>不行|不可以|不要|無法|不能</code> 跟「不是 X、是 Y」掃描通過、但 reviewer A 抓出「核心責任不是 X、而是 Y」變體段首（佔 12 處）</li>
<li>05 自掃描通過、但 reviewer A 仍抓出「沒有 X、會 Y」鏈式負向句構 + 「四步驟模板」+ 「case 引用框架取代商業邏輯先行」三類新 pattern</li>
<li>06 self-scan 加了「不是 X、而是 Y」變體 + 「沒有 X 會 Y」、仍漏掉「對應 [case]：揭露 N 個機制」段首取代核心概念句的 pattern（reviewer A 抓 45 issue、其中 11/12 新段都犯這個錯）</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>每個模組 reviewer 抓出新 pattern 後、回頭更新 self-scan regex</li>
<li>把 self-scan 視為持續演進的工具、不是固定 checklist</li>
<li>Stage 5 polish pass 是處理自掃描盲點累積的標準入口（見前段）</li>
<li>06 模組後 self-scan 加 <code>rg -n &quot;^對應 \[&quot; &lt;module-paths&gt;</code> 抓段首 case 引用框架</li>
</ul>
<h3 id="陷阱-6case-引用段首取代核心概念句06-模組新發現">陷阱 6：Case 引用段首取代核心概念句（06 模組新發現）</h3>
<p>LLM 從 case 反推內容時、容易把 case 揭露當概念出發點、寫成「對應 [case]：揭露 N 個機制 — &hellip;」段首結構。讀者尚未理解概念就被丟入案例細節、且跨章讀同句構會感同質。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>06 模組 12 個新段中 11 個用「對應 [case]：揭露 N 個機制」相同句構作為 section 第二段</li>
<li>概念定義句被推到第二段或更後、商業邏輯先於 case 的原則被推翻</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>把 case 引用視為「三段式」結構：概念定義句 → case 引用 → 通用展開</li>
<li>寫每段時、先確認段首是「該概念是什麼、承擔什麼責任」、case 引用退到第二位置</li>
<li>Case 引用句構應變化：寫多章時刻意避免同句構連續超過 3 次</li>
<li>詳見 skill 內部原則卡 <code>principles/case-citation-three-part</code>（對應檔案 <code>.claude/skills/case-first-module-workflow/references/principles/case-citation-three-part.md</code>、屬 skill 內部 reference、不對外暴露）</li>
</ul>
<h3 id="陷阱-7medium-case-實作層擴寫過頭06-模組新發現">陷阱 7：Medium case 實作層擴寫過頭（06 模組新發現）</h3>
<p>Medium case（30-50 行、結構化但無具體數字）首次套用時、容易把 case 沒提的具體實作層擴寫進章節、把通用工程知識掛到 case 名下。</p>
<p><strong>實證</strong>：</p>
<ul>
<li>06 模組 6.12 idempotency-replay 從 S1「key 設計要跟業務邊界一致」一條方向擴寫成「key 來源 / TTL / fallback / 偽造防護 / 5 個 observability 欄位」5 條實作判讀、case 沒提這些細節</li>
<li>06 模組 6.14 dependency-reliability-budget 從 M1 region failover 擴寫成「thundering herd」機制名 + 「先恢復核心 region 最小集合」具體步驟、case 沒提這兩個</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>Medium case 引用用 <em>mechanism 名稱</em> 精準引用、不擴寫到 case 沒提的具體實作細節</li>
<li>引用後若要展開實作層、用「以下實作層判讀屬通用工程知識展開、case 本身只給 X 方向」明示分層</li>
<li>Case fidelity reviewer 的 prompt 要特別點出 medium case 的「實作層擴寫」失分類型</li>
</ul>
<h3 id="陷阱-8跨-case-合成-frame-升級成-case-揭露07-模組新發現">陷阱 8：跨 case 合成 frame 升級成 case 揭露（07 模組新發現）</h3>
<p>當段落把多個 case 的失效訊號抽象為更高層 frame（如「跨工具回查壓力」「平台責任切分」）、LLM 會把章節合成的 frame 包裝成 case 揭露。讀者回查 case 時會發現章節說的「case 揭露 X」實際是章節 derive、不是 case 原文框架。</p>
<p>跟陷阱 1（skeleton case 擴寫成 case 事實）跟陷阱 4（rich case 判讀層當 fact）的差別：</p>
<ul>
<li><strong>陷阱 1</strong>：case 沒提的細節（具體數字、taxonomy）被寫成 case 揭露</li>
<li><strong>陷阱 4</strong>：case 有提、但屬作者判讀層的內容被寫成 case fact</li>
<li><strong>陷阱 8</strong>：case <em>單獨</em> 寫的訊號被章節 <em>跨 case 合成</em> 抽象為更高層 frame、frame 本身不在任一 case 原文</li>
</ul>
<p><strong>實證</strong>（07 batch 1 reviewer B 抓的 2 個 high issue）：</p>
<ul>
<li>7.7 跨工具回查壓力：Uber 失效控制面寫「告警串接不足」、Slack 寫「訊號未匯流」— 都是單工具內訊號、章節合成「跨工具回查」axis</li>
<li>7.7 平台責任切分：SolarWinds 失效控制面寫「更新來源信任過於單點」「行為監測難以區分合法元件」— 都是供應鏈信任議題、章節合成「平台 vs 產品 audit 責任分離」frame</li>
</ul>
<p><strong>防範</strong>：</p>
<ul>
<li>段落把多 case 抽象為更高層 frame 時、要 explicit 標明「frame 是本章合成、case 原文沒有此 frame」</li>
<li>修法範例：「兩個案例分別在 X 層揭露同類失效訊號 — A case 標明 B、C case 標明 D。本章把兩者抽象為『XXX』是 YYY 視角的合成 frame、非 case 原文框架。」</li>
<li>Stage 3 reviewer B prompt 要明示「跨 case 合成 frame 必須標為本章合成」是 high 級 issue 抓取項</li>
</ul>
<h3 id="陷阱-9case-引用句構同質化07-模組新發現">陷阱 9：Case 引用句構同質化（07 模組新發現）</h3>
<p>即使遵守 case 引用三段式紀律、跨章節 case 引用仍會出現句構同質化。13 處 case 引用 11 處用同一句構「揭露 N 層失效控制面 — A、B、C。案例『可落地檢查點』標明 mechanism 為 X、前提是 Y」。讀者跨章連讀時、會把 case 引用當儀式而非論證。</p>
<p><strong>實證</strong>：07 batch 1 reviewer A 抓出 systemic medium issue (Issue 8.1)、13 段 case 引用 11 段用相同句構。Stage 5 polish pass 主動分流 4 處後狀況改善。</p>
<p><strong>防範</strong>：</p>
<ul>
<li>句構選擇要 <em>跟著 case 類型走</em>、不是隨機變化（case 直接列 N mechanism → 「揭露 N 層」；case 揭露單一壓力 → 「補的失效訊號是 X」；case 揭露對比 → 「揭露兩個層次的對照」）</li>
<li>Stage 5 polish pass 加句構分流為標準工序之一（跟負向骨架同層級）</li>
<li>自掃描 regex <code>^對應 \[</code> 抓不到此類問題（這是符合三段式的引用、只是句構單一）、要靠 stage 5 主動 scan：<code>rg -c &quot;揭露[^。]*失效控制面&quot; &lt;module-paths&gt;</code> 看同句構出現次數、超過 5 處要分流</li>
</ul>
<h3 id="章節已有-routing-skeleton的特殊處理07-模組新發現">「章節已有 routing skeleton」的特殊處理（07 模組新發現）</h3>
<p>07 模組跟 06 / 09 不同之處：章節在 stage 2 前已有完整 routing layer 結構（threat scope / 從本章到實作 / 問題節點表 / 風險邊界 / 案例觸發 / 路由）— stage 2 是在現有結構內補 case-driven 深化段，而非空白擴章。</p>
<p>這個情境下：</p>
<ul>
<li><strong>SSoT 衝突更容易發生</strong>：新段落要跟既有章節結構協調、不只是新增內容。07 batch 1 三個 H issue（C-H1/H2/H3）都是 frame 跟既有章節 / 其他章節新增段衝突</li>
<li><strong>章節寫作邊界要先確認</strong>：補強段聚焦在「現有問題節點表的 mechanism 深化」、不擴成厚重 case-driven 章節（避免章節結構失衡）</li>
<li><strong>Cross-link 密度顯著上升</strong>：補強段要明示「本節聚焦 X 視角、canonical 在 Y 章」、否則 reviewer C 會抓 frame 重複展開</li>
</ul>
<p>判讀條件：</p>
<ul>
<li>章節已有 threat scope / 問題節點表 / 案例觸發段 → 走「補強段」策略、不空白擴章</li>
<li>章節是 routing layer / 導讀性質、不適合 case-driven 深化 → 跳過本流程</li>
<li>章節有 case 庫但 case 主要是 skeleton 型（30 行 frame） → 補強段嚴守「揭露 X 方向、通用補充」紀律、不擴寫實作層</li>
</ul>
<h3 id="衍生-insightreviewer-維度沒覆蓋的部分">衍生 insight：reviewer 維度沒覆蓋的部分</h3>
<p>3 個模組跑下來、發現現有 3 reviewer 維度（規範 / 案例準確性 / 跨章一致性）有未覆蓋的問題：</p>
<ul>
<li><strong>教學性 / 讀者路徑</strong>：章節之間的閱讀順序是否合理？讀者讀完 A 章能不能銜接 B 章？目前沒 reviewer 檢查</li>
<li><strong>判讀條件可操作性</strong>：寫了判讀訊號、但實際工程師能不能用這些訊號做決策？沒 reviewer 驗證</li>
<li><strong>實作可行性</strong>：建議的設計是否真的能落地？跨團隊協調是否現實？需要懂業務的 reviewer</li>
</ul>
<p>未來 6 / 7 / 8 模組執行時、可以考慮加第 4 個 reviewer 維度（教學性 + 實作可行性）。</p>
<h2 id="跟其他寫作流程的差異">跟其他寫作流程的差異</h2>
<p>跟「LLM 自生 + 人工 review」比、本流程的差異：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>LLM 自生 + 人工 review</th>
          <th>Case-first + Agent team</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope 來源</td>
          <td>訓練資料 + 提示詞</td>
          <td>真實案例 findings</td>
      </tr>
      <tr>
          <td>準確性檢查</td>
          <td>人工讀完對比</td>
          <td>Case fidelity reviewer 自動對照</td>
      </tr>
      <tr>
          <td>規範遵循</td>
          <td>人工 checklist</td>
          <td>Standards reviewer 自動掃描</td>
      </tr>
      <tr>
          <td>跨章一致性</td>
          <td>人工跨檔 grep</td>
          <td>Consistency reviewer 自動檢查</td>
      </tr>
      <tr>
          <td>Context 成本</td>
          <td>低（人工不佔 LLM context）</td>
          <td>中（reviewer 各自佔自己 context、主 context 輕）</td>
      </tr>
      <tr>
          <td>時間成本</td>
          <td>高（人工逐段讀）</td>
          <td>中（reviewer 平行）</td>
      </tr>
      <tr>
          <td>真實事故揭露</td>
          <td>受限於 reviewer 經驗</td>
          <td>受限於案例庫覆蓋</td>
      </tr>
  </tbody>
</table>
<p>跟「LLM 自生 + 自我 review」比：</p>
<ul>
<li>自我 review 抓不到自生內容的盲點（self-blindness）</li>
<li>Agent team 是 <em>不同 instance</em>、不共享 context、能扮演獨立 reviewer</li>
</ul>
<h2 id="下一步">下一步</h2>
<p>本流程在 backend/01 至 backend/07 batch 1 七個模組驗證後（共 ~45 章 / 385 review issue / case fidelity 70-93% 區間）、方法論已工具化為 <code>case-first-module-workflow</code> skill（內部檔 <code>.claude/skills/case-first-module-workflow/</code>、含 stage 1-5 流程、reviewer prompt template、self-scan regex 跟 5 個原則卡）、後續套用到：</p>
<ul>
<li>backend/07 batch 2 LLM 安全：case 庫缺位（OWASP LLM Top 10 + agent injection 公開事件未累積成模組 case）、要先建 LLM case 庫再走 case-first</li>
<li>backend/07 batch 3 治理章節：routing 層 / 導讀性質、case-driven 深化適用度低、做標準 polish pass 即可</li>
<li>backend/08 incident response：跟 04 / 06 / 07 cross-link 密度最高、SSoT 對應規劃壓力最大</li>
<li>其他模組依此類推</li>
</ul>
<p>06 模組是首次套用工具化 skill 的模組、驗證 skill 對 stage 1-2 加速有效、但 reviewer A 仍抓出 45 issue（高於 05 之前 baseline 20-30、推動 v1.2 把 standards reviewer baseline 擴大到 20-45）— 揭露 skill 改進方向（self-scan regex 需要持續演進、case 引用段首結構是 LLM 系統性傾向）。</p>
<p>07 batch 1 驗證下「章節已有 routing skeleton」情境的處理策略：補強段不擴成厚重 case-driven 章節、聚焦 mechanism 深化 + cross-link 對齊。揭露兩個新陷阱（跨 case 合成 frame 升級成 case 揭露、case 引用句構同質化）、補進 skill 跟方法論。</p>
<p>流程本身會在每個模組後 retrospective、看 reviewer 維度是否該調整、findings 抽取方法是否該強化、polish pass 處理 pattern 是否該擴充。目前已知改進方向：</p>
<ul>
<li>加 reviewer：教學性審查（讀者路徑是否清楚、判讀順序是否合理）</li>
<li>強化 findings 抽取：標註 finding 的 <em>泛化程度</em>、避免把 case-specific 細節推為通用結論</li>
<li>Rich / Medium case 引用紀律：把「fact vs derive」分層 + 「mechanism 名稱精準引用」寫進 stage 1 抽 findings 模板、stage 3 case fidelity reviewer prompt 也明示此分界</li>
<li>自掃描 regex 持續演進：每個模組 reviewer 抓出新 pattern 後、回頭加進 self-scan 工具、避免在下個模組重蹈覆轍。06 模組後加 <code>^對應 \[</code> 抓段首 case 引用框架。07 模組後標明 <code>^對應 \[</code> 在三段分離結構下會 false positive、要靠 awk 看 prev line context</li>
<li>Case 引用三段式：把「概念定義 → case 引用 → 通用展開」當段落結構紀律、避免段首被 case 引用取代（06 模組最大宗 systemic 違規）</li>
<li>Case 引用句構分流：07 模組後 stage 5 polish pass 加句構分流為標準工序、避免跨章 13+ 段同句構讀感儀式化</li>
<li>跨 case 合成 frame 紀律：07 模組後 reviewer B prompt 明示「跨 case 合成 frame 必須標為本章合成」是 high 級 issue</li>
<li>加修正後自動 lint：修完不只跑 mdtools、加跑「找首句否定句」「找表格沒延伸」「找模板化並列點」「找段首 case 引用」的自動掃描</li>
</ul>
<p>跟其他寫作協議的整合：本流程跟 <code>compositional-writing</code> skill 互補（後者管 <em>單篇</em> 寫作的原子化跟意圖、本流程管 <em>跨章模組</em> 的 scope 跟一致性）、跟 <code>requirement-protocol</code> skill 互補（後者管 <em>對話協議</em>、本流程管 <em>內容生產</em>）。</p>
]]></content:encoded></item><item><title>Commit message vs source code doc：兩份不同職責的文件</title><link>https://tarrragon.github.io/blog/record/commit-message-vs-source-code-doc%E5%85%A9%E4%BB%BD%E4%B8%8D%E5%90%8C%E8%81%B7%E8%B2%AC%E7%9A%84%E6%96%87%E4%BB%B6/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/commit-message-vs-source-code-doc%E5%85%A9%E4%BB%BD%E4%B8%8D%E5%90%8C%E8%81%B7%E8%B2%AC%E7%9A%84%E6%96%87%E4%BB%B6/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：source code doc 寫給「未來的讀者」，commit message 寫給「想了解過去發生什麼的考古者」。兩者是不同文件，內容該分開。
&lt;strong>設計原則&lt;/strong>：時序敏感的資訊（為什麼這次改動、考慮過什麼方案）放 commit；持續適用的資訊（當前契約、不變量）放 source。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../function-doc-layered-design/">函式文件分層設計&lt;/a> 反模式 3「過去式 doc」的展開——把「source 跟 commit message 的時序職責邊界」拉成獨立主題討論。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點兩份文件的職責容易被混在一起">起點：兩份文件的職責容易被混在一起&lt;/h2>
&lt;p>Source code doc 的職責是「描述當前 code 的契約跟行為」、commit message 的職責是「描述某次改動做了什麼跟為什麼做」——兩者讀者不同、時序屬性不同、本該各歸各家。實務上這兩份文件的職責經常被混在 source code doc 一處：source 變成所有歷史的垃圾桶、commit message 反而沒人認真寫。&lt;/p>
&lt;p>實務上常看到的污染：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 修了 issue #123 的 race condition
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 從 v2.3 開始改用 lock-free 結構
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">/// TODO: @alice 之後可能要改用 SkipList
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">process&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 doc 混了三類資訊：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>過去發生什麼&lt;/strong>（修了 issue #123）→ 屬於 commit message&lt;/li>
&lt;li>&lt;strong>過去做過什麼決定&lt;/strong>（v2.3 開始改用 lock-free）→ 屬於 commit message / changelog&lt;/li>
&lt;li>&lt;strong>未來可能要改什麼&lt;/strong>（TODO @alice 改用 SkipList）→ 屬於 issue tracker / TODO 系統&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>沒有一條是「未來讀者讀這份 code 需要的資訊」&lt;/strong>——三條都凍結在過去某一刻、source 卻被當成歷史快照在用。要釐清這個問題、得先想清楚兩種文件各自的讀者與時間性。&lt;/p>
&lt;hr>
&lt;h2 id="時序差異當前狀態-vs-狀態轉移">時序差異：當前狀態 vs 狀態轉移&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文件&lt;/th>
 &lt;th>描述什麼&lt;/th>
 &lt;th>寫給誰讀&lt;/th>
 &lt;th>時間性&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Source code doc&lt;/td>
 &lt;td>當前 code 的契約、行為、不變量&lt;/td>
 &lt;td>即將呼叫 / 修改 code 的人&lt;/td>
 &lt;td>&lt;strong>持續適用&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Commit message&lt;/td>
 &lt;td>這次改動做了什麼、為什麼做&lt;/td>
 &lt;td>想了解某個變動的考古者&lt;/td>
 &lt;td>&lt;strong>特定時間點的決定&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵差別是&lt;strong>時間性&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Source code doc 描述「&lt;strong>現在&lt;/strong>這份 code 在做什麼」——只要 code 不變，doc 就持續有效&lt;/li>
&lt;li>Commit message 描述「&lt;strong>那一刻&lt;/strong>為什麼要改 code」——commit 完成的那一秒就成為歷史&lt;/li>
&lt;/ul>
&lt;p>把過去式的內容塞進 source code doc，會讓 doc 變成「凍結在某個歷史時點的快照」，而不是描述當前狀態。&lt;/p>
&lt;hr>
&lt;h2 id="該寫在-commit-message-的內容">該寫在 commit message 的內容&lt;/h2>
&lt;p>Commit message 的核心職責是回答「&lt;strong>這次改動做了什麼、為什麼做&lt;/strong>」——所有「凍結在某次提交時點」的資訊都應該住在這裡、而不是被塞進 source 變成過時快照。下面四類是最常被誤放進 source 的內容：&lt;/p>
&lt;h3 id="1-改動的動機為什麼這次要動">1. 改動的動機（為什麼這次要動）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">fix: prevent double-charge on payment retry
&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">Payment gateway 對同一個 transaction_id 會回傳 200 但實際扣款兩次
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">（incident #4521）。在 client 端加上 idempotency_key，gateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">看到重複的 key 直接回 cached response。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「為什麼動」幾乎永遠屬於 commit message。&lt;strong>source code 只需要描述「現在的行為是什麼」，不需要解釋「過去為什麼變成這樣」&lt;/strong>——除非那個「為什麼」對未來呼叫者仍是必須知道的限制（見後面段落）。&lt;/p>
&lt;h3 id="2-評估過的替代方案why-not-x">2. 評估過的替代方案（why not X）&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">refactor: replace stream with reactive value
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">考慮過三個方案：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">- A. 改成 broadcast stream：最 minimal，但保留同樣的 payload 語義模糊問題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">- B. 加新 broadcast stream 平行存在：兩條 stream 容易不同步
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">- C. 拆成 reactive value（採用）：與系統其他 service 一致、消除多訂閱問題
&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">選 C 因為與 codebase 其他 service 風格對齊，雖然改動範圍最大。&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「考慮過 A、B、C，選了 C」這類資訊對 reviewer 重要，對未來讀 code 的人多半不重要——他們看到的是 C 的結果，不關心你考慮過 A、B。&lt;strong>這類資訊屬於 commit message / PR description&lt;/strong>，不屬於 source code doc。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：source code doc 寫給「未來的讀者」，commit message 寫給「想了解過去發生什麼的考古者」。兩者是不同文件，內容該分開。
<strong>設計原則</strong>：時序敏感的資訊（為什麼這次改動、考慮過什麼方案）放 commit；持續適用的資訊（當前契約、不變量）放 source。</p></blockquote>
<blockquote>
<p>本篇是 <a href="../function-doc-layered-design/">函式文件分層設計</a> 反模式 3「過去式 doc」的展開——把「source 跟 commit message 的時序職責邊界」拉成獨立主題討論。</p></blockquote>
<hr>
<h2 id="起點兩份文件的職責容易被混在一起">起點：兩份文件的職責容易被混在一起</h2>
<p>Source code doc 的職責是「描述當前 code 的契約跟行為」、commit message 的職責是「描述某次改動做了什麼跟為什麼做」——兩者讀者不同、時序屬性不同、本該各歸各家。實務上這兩份文件的職責經常被混在 source code doc 一處：source 變成所有歷史的垃圾桶、commit message 反而沒人認真寫。</p>
<p>實務上常看到的污染：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 修了 issue #123 的 race condition
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 從 v2.3 開始改用 lock-free 結構
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// TODO: @alice 之後可能要改用 SkipList
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>這段 doc 混了三類資訊：</p>
<ol>
<li><strong>過去發生什麼</strong>（修了 issue #123）→ 屬於 commit message</li>
<li><strong>過去做過什麼決定</strong>（v2.3 開始改用 lock-free）→ 屬於 commit message / changelog</li>
<li><strong>未來可能要改什麼</strong>（TODO @alice 改用 SkipList）→ 屬於 issue tracker / TODO 系統</li>
</ol>
<p><strong>沒有一條是「未來讀者讀這份 code 需要的資訊」</strong>——三條都凍結在過去某一刻、source 卻被當成歷史快照在用。要釐清這個問題、得先想清楚兩種文件各自的讀者與時間性。</p>
<hr>
<h2 id="時序差異當前狀態-vs-狀態轉移">時序差異：當前狀態 vs 狀態轉移</h2>
<table>
  <thead>
      <tr>
          <th>文件</th>
          <th>描述什麼</th>
          <th>寫給誰讀</th>
          <th>時間性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source code doc</td>
          <td>當前 code 的契約、行為、不變量</td>
          <td>即將呼叫 / 修改 code 的人</td>
          <td><strong>持續適用</strong></td>
      </tr>
      <tr>
          <td>Commit message</td>
          <td>這次改動做了什麼、為什麼做</td>
          <td>想了解某個變動的考古者</td>
          <td><strong>特定時間點的決定</strong></td>
      </tr>
  </tbody>
</table>
<p>關鍵差別是<strong>時間性</strong>：</p>
<ul>
<li>Source code doc 描述「<strong>現在</strong>這份 code 在做什麼」——只要 code 不變，doc 就持續有效</li>
<li>Commit message 描述「<strong>那一刻</strong>為什麼要改 code」——commit 完成的那一秒就成為歷史</li>
</ul>
<p>把過去式的內容塞進 source code doc，會讓 doc 變成「凍結在某個歷史時點的快照」，而不是描述當前狀態。</p>
<hr>
<h2 id="該寫在-commit-message-的內容">該寫在 commit message 的內容</h2>
<p>Commit message 的核心職責是回答「<strong>這次改動做了什麼、為什麼做</strong>」——所有「凍結在某次提交時點」的資訊都應該住在這裡、而不是被塞進 source 變成過時快照。下面四類是最常被誤放進 source 的內容：</p>
<h3 id="1-改動的動機為什麼這次要動">1. 改動的動機（為什麼這次要動）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">fix: prevent double-charge on payment retry
</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">Payment gateway 對同一個 transaction_id 會回傳 200 但實際扣款兩次
</span></span><span class="line"><span class="ln">4</span><span class="cl">（incident #4521）。在 client 端加上 idempotency_key，gateway
</span></span><span class="line"><span class="ln">5</span><span class="cl">看到重複的 key 直接回 cached response。</span></span></code></pre></div><p>「為什麼動」幾乎永遠屬於 commit message。<strong>source code 只需要描述「現在的行為是什麼」，不需要解釋「過去為什麼變成這樣」</strong>——除非那個「為什麼」對未來呼叫者仍是必須知道的限制（見後面段落）。</p>
<h3 id="2-評估過的替代方案why-not-x">2. 評估過的替代方案（why not X）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">refactor: replace stream with reactive value
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">考慮過三個方案：
</span></span><span class="line"><span class="ln">4</span><span class="cl">- A. 改成 broadcast stream：最 minimal，但保留同樣的 payload 語義模糊問題
</span></span><span class="line"><span class="ln">5</span><span class="cl">- B. 加新 broadcast stream 平行存在：兩條 stream 容易不同步
</span></span><span class="line"><span class="ln">6</span><span class="cl">- C. 拆成 reactive value（採用）：與系統其他 service 一致、消除多訂閱問題
</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">選 C 因為與 codebase 其他 service 風格對齊，雖然改動範圍最大。</span></span></code></pre></div><p>「考慮過 A、B、C，選了 C」這類資訊對 reviewer 重要，對未來讀 code 的人多半不重要——他們看到的是 C 的結果，不關心你考慮過 A、B。<strong>這類資訊屬於 commit message / PR description</strong>，不屬於 source code doc。</p>
<h3 id="3-migration--部署相關步驟">3. Migration / 部署相關步驟</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">feat: migrate user_profile from int_id to uuid
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl">注意：
</span></span><span class="line"><span class="ln">4</span><span class="cl">- 跑 migration 0042 之前先確認所有 client 已升到 v3.2 以上
</span></span><span class="line"><span class="ln">5</span><span class="cl">- migration 預估 2 小時（10M rows），建議週末執行
</span></span><span class="line"><span class="ln">6</span><span class="cl">- rollback：reverse migration 0042 然後 redeploy v3.1</span></span></code></pre></div><p>部署時序與步驟是當下發布動作的一部分，commit / release notes 該寫；source code 不該背這個負擔。</p>
<h3 id="4-bug-號ticket-連結incident-紀錄">4. Bug 號、ticket 連結、incident 紀錄</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">fix: handle empty cart in checkout button visibility
</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">Closes <span class="c1">#1234</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">Related: incident-2026-04-12 <span class="o">(</span>button stuck enabled<span class="o">)</span></span></span></code></pre></div><p>把 ticket 號 / issue 連結寫在 commit message，git blame 出來的 commit 直接帶你去原始討論。寫在 source code 反而會 outdated（issue 關了、tracker 換了、URL 改了）。</p>
<hr>
<h2 id="該寫在-source-code-doc-的內容">該寫在 source code doc 的內容</h2>
<p>Source code doc 的核心職責是描述「<strong>當前 code 的契約跟行為</strong>」——只要 code 不變、doc 就持續有效。下面四類是「持續適用」的資訊類別、屬於 source 的家：</p>
<h3 id="1-當前對外契約">1. 當前對外契約</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 從本地購物車移除指定商品
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">///
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// 找不到對應品項時不做事；不會拋例外。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">);</span></span></span></code></pre></div><p>這是「現在這個 function 對 caller 承諾什麼」——持續適用，跟「上週為什麼加這個 function」無關。</p>
<h3 id="2-隱性需求--必要的呼叫順序">2. 隱性需求 / 必要的呼叫順序</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 必須在 [init] 之後呼叫；否則 throw `StateError`。
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>「呼叫順序」是當前 code 的契約限制，未來呼叫者必須遵守。屬於 source code doc。</p>
<h3 id="3-對未來讀者仍然重要的過去原因">3. 對未來讀者仍然重要的「過去原因」</h3>
<p>少數情況下，「為什麼以前這樣決定」對未來讀者<strong>仍是必要資訊</strong>——典型是「這個寫法看起來怪，但有非顯然的原因」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">processPayment</span><span class="p">(</span><span class="n">Payment</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 刻意不 retry —— payment gateway 是非冪等，retry 會造成重複扣款
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// （見 incident-2026-04-12）。失敗一律拋給上層人工處理。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="k">return</span> <span class="n">_gateway</span><span class="p">.</span><span class="n">charge</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這條註解兼具「歷史原因」和「持續適用的限制」——未來維護者看到這段 code 會想「為什麼沒 retry？」，這條註解防止他「順手加上」。<strong>這類兼具兩種性質的內容是少數該留在 source 的歷史相關 doc</strong>。</p>
<p>判斷標準：「未來讀者<strong>不知道這條歷史會做錯決定</strong>嗎？」</p>
<ul>
<li>是 → 留 source</li>
<li>不是 → 留 commit</li>
</ul>
<h3 id="4-不變量--invariant">4. 不變量 / invariant</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">class</span> <span class="nc">CircularBuffer</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">/// 元素數量永遠在 [0, capacity] 之間
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kt">int</span> <span class="kd">get</span> <span class="n">length</span> <span class="o">=&gt;</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不變量是「這個型別永遠成立的事實」，是契約的一部分，屬於 source。</p>
<hr>
<h2 id="反模式">反模式</h2>
<h3 id="反模式-1把-commit-message-內容塞進-source">反模式 1：把 commit message 內容塞進 source</h3>
<p><strong>正向概念</strong>：source code doc 描述「現在的行為」、git log 才是「歷史演進」的家。兩者各自有對應的工具（IDE 看 doc、<code>git log</code> 看演進）、各司其職就能讓兩邊都精準。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 反：寫成歷史紀錄
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 2024-01-15 加上 retry 邏輯
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 2024-03-22 改用 exponential backoff
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">/// 2024-07-08 加上 jitter 避免 thundering herd
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span> <span class="n">fetch</span><span class="p">(</span><span class="kt">String</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 正：source 只寫當前行為
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">/// 自動 retry 失敗的請求，使用 exponential backoff + jitter
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">Response</span><span class="o">&gt;</span> <span class="n">fetch</span><span class="p">(</span><span class="kt">String</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="o">//</span> <span class="err">演進歷史在</span> <span class="n">git</span> <span class="n">log</span> <span class="err">看</span></span></span></code></pre></div><p>把所有歷史塞進 source 等於在 source code 重做一份 git log——但 git log 已經存在、且結構化、可搜尋、有 author / timestamp。重做一份在 source 只會 outdated（下次再加邏輯時忘了補日期就破功）、而 git log 永遠是同步的。</p>
<h3 id="反模式-2commit-message-只寫-update--fix">反模式 2：commit message 只寫 &ldquo;update&rdquo; / &ldquo;fix&rdquo;</h3>
<p><strong>正向概念</strong>：commit message 是給未來考古者的線索——<code>git blame</code> 跳到一個 commit 時、message 是讀者拿到的第一份資訊。寫得清楚、考古路徑就短；寫得模糊、考古者得繼續挖 PR / 找原作者問。</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">- update
</span></span><span class="line"><span class="ln">2</span><span class="cl">- fix
</span></span><span class="line"><span class="ln">3</span><span class="cl">- wip
</span></span><span class="line"><span class="ln">4</span><span class="cl">- final
</span></span><span class="line"><span class="ln">5</span><span class="cl">- final v2
</span></span><span class="line"><span class="ln">6</span><span class="cl">- final v2 真的</span></span></code></pre></div><p>這類 commit message 當下就沒人看得懂、半年後 <code>git blame</code> 把人帶到 message 寫 &ldquo;update&rdquo; 的 commit、等於把讀者帶到死巷。合理 commit message 的最小單位是 <code>&lt;type&gt;: &lt;one-line summary&gt;</code>、例如 <code>fix: handle empty cart in checkout</code>——一行就好、但要說清楚做了什麼。</p>
<h3 id="反模式-3source-code-doc-寫滿-todo--fixme">反模式 3：source code doc 寫滿 TODO / FIXME</h3>
<p><strong>正向概念</strong>：「想未來改但還沒改」屬於 issue tracker——issue tracker 有優先序、有 owner、有 due date、能被排程。source code 的 TODO 沒有這些屬性、會被慢慢遺忘。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// TODO: refactor to use streams
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// FIXME: handle null case
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// HACK: temporary workaround for issue #234
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">/// XXX: this is broken under high load
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">doSomething</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>這些都是「想未來改但還沒改」的事——把它們留在 source 有三個問題：</p>
<ul>
<li>TODO 在 source 不會被 prioritize（產品 / 專案管理工具看不到 source 內的 TODO）</li>
<li>FIXME 在 source 容易被忽略（讀的人會想「不是我寫的不是我的問題」）</li>
<li>HACK / XXX 警告<strong>只在第一次讀時有效</strong>、第二次讀的人會麻木</li>
</ul>
<p>問題嚴重需要立刻處理 → 開 ticket、commit fix；不嚴重可以等 → 開 backlog ticket、source 別寫。把待辦項從 source 搬到 issue tracker、會被真正當成「待辦」處理。</p>
<h3 id="反模式-4把-pr-description-抄一份進-source">反模式 4：把 PR description 抄一份進 source</h3>
<p><strong>正向概念</strong>：PR description 是「這次提交的時空快照」、source code doc 是「持續適用的當前契約」。兩者描述的是同一段 code 在不同時序下的不同切面、各自有對應的家。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 這個 function 是為了支援新的 multi-currency 結帳流程。
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 詳細需求見 PR #4521 與設計文件 https://wiki.../...
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// 業務需求：客戶可以混合多幣別商品結帳，結帳當下統一換算成 settlement currency。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">/// QA 已驗證 5 種主要幣別組合 + 邊界 case。
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">multiCurrencyCheckout</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>PR description 該寫的內容（業務脈絡、設計連結、QA 範圍）抄進 source、會讓 source 凍結在「<strong>這次新增時的時空狀態</strong>」——半年後 PR 已經是歷史、連結可能失效、QA 範圍可能擴展、但 source 還停在那一刻。PR description 留在 PR、source 只寫 function 當前的對外契約。</p>
<hr>
<h2 id="git-blame-archaeology-workflow">Git blame archaeology workflow</h2>
<p>當 source code doc 跟 commit message 各司其職時，<strong>考古工作流</strong>會變得清晰：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">讀者看到一段 code 不懂為什麼這樣寫
</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">先看 source code doc
</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">不夠 → 跑 git blame
</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">找到引入這段 code 的 commit
</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">讀 commit message
</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">不夠 → 點進去看完整 PR / issue</span></span></code></pre></div><p>這個工作流要能順利跑，前提是：</p>
<ol>
<li><strong>commit 顆粒度合理</strong>——一個 commit 一個邏輯改動，不要「fix typo + refactor + add feature」混在一起，否則 blame 出來看到一個改 50 個檔案的 commit，message 寫 &ldquo;stuff&rdquo;，等於沒線索</li>
<li><strong>commit message 寫清楚動機</strong>——不是「changed X」（git diff 看得出來），而是「changed X <strong>because Y</strong>」</li>
<li><strong>重大決定用 PR 描述補充</strong>——commit message 太長不適合塞長文，PR description 是放長文的地方</li>
</ol>
<p>如果這三點做到，未來讀 code 的人有一條清楚的考古路徑，不必逼 source code doc 背所有歷史。</p>
<hr>
<h2 id="一個分配工具">一個分配工具</h2>
<p>決定一條資訊放哪時，問三個問題：</p>
<ol>
<li><strong>「未來讀者不知道這條會做錯決定嗎？」</strong>
<ul>
<li>是 → source code doc</li>
<li>不是 → commit message</li>
</ul>
</li>
<li><strong>「這條描述的是當前的行為，還是某次轉移？」</strong>
<ul>
<li>當前行為 → source code doc</li>
<li>某次轉移 → commit message</li>
</ul>
</li>
<li><strong>「Code 改了，這條會不會 outdated？」</strong>
<ul>
<li>不會（描述當前狀態）→ source code doc</li>
<li>會（描述特定時間點）→ commit message</li>
</ul>
</li>
</ol>
<p>三個問題收斂到同一個直覺：<strong>「凍結在過去」屬於 commit、「持續適用」屬於 source</strong>。</p>
<hr>
<h2 id="邊界什麼時候-source-還是該帶歷史脈絡">邊界：什麼時候 source 還是該帶歷史脈絡</h2>
<p>「歷史進 commit、契約進 source」是預設、<strong>但有些情境 source 還是該保留歷史脈絡</strong>——共通特徵是「未來讀者不知道這段歷史會做錯決定」：</p>
<ul>
<li><strong>看似怪、但有非顯然原因的寫法</strong>：「刻意不 retry、payment gateway 是非冪等」——下個維護者順手加 retry 會出事</li>
<li><strong>跟非預期外部行為對齊的 workaround</strong>：「拆兩步 query 避開 SQLite 32-bit Android 的 integer overflow（issue #1234）」——讀者重構時會想「為什麼不一次查」</li>
<li><strong>保留某段 code 的合規 / 法務原因</strong>：「依 GDPR 留 30 天可恢復、不是直接刪」——縮短到 7 天會違反法規</li>
<li><strong>效能調優的非顯然參數</strong>：「batch size = 32 是 production 跑出來的甜蜜點、改大會 OOM」——下次 review 看到「為什麼不開大」時得知道過去的實驗結果</li>
</ul>
<p>判斷標準：「未來讀者<strong>不知道這條歷史就會做錯決定</strong>嗎？」答「是」就留在 source、答「不是」就留在 commit。</p>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>Source code doc 寫給「<strong>正要動這段 code 的人</strong>」、commit message 寫給「<strong>想知道為什麼當初這樣寫的人</strong>」。</p></blockquote>
<p>寫東西之前先問：我寫這段，是要幫<strong>正要動 code 的人</strong>做對決定，還是要幫<strong>回顧歷史的人</strong>理解某次改動？兩個讀者要找的資訊不同，分成兩處寫，雙方都受惠。</p>
<hr>
<h2 id="收束兩份文件協同源頭就要分清楚">收束：兩份文件協同，源頭就要分清楚</h2>
<p>很多團隊抱怨「source code doc 太亂、commit message 沒人寫」，本質是這兩份文件的職責沒分清楚。Source 想包辦所有事就會充滿過時內容；commit message 沒人寫是因為「反正歷史會寫進 source」變成預設。</p>
<p>把兩者的職責分清楚，兩份文件都會變健康：</p>
<ul>
<li><strong>source 變短、變精準</strong>：只寫當前契約，doc 不會 outdated</li>
<li><strong>commit message 被認真寫</strong>：因為它是某些資訊的唯一家</li>
<li><strong>考古路徑清楚</strong>：blame → commit → PR 是可預期的回溯路徑</li>
</ul>
<p>寫 doc / 寫 commit 是同一個技能的兩面。不要把任何一邊當成另一邊的替代品。</p>
]]></content:encoded></item><item><title>函式文件分層設計：型別、介面、實作各自該寫什麼</title><link>https://tarrragon.github.io/blog/record/%E5%87%BD%E5%BC%8F%E6%96%87%E4%BB%B6%E5%88%86%E5%B1%A4%E8%A8%AD%E8%A8%88%E5%9E%8B%E5%88%A5%E4%BB%8B%E9%9D%A2%E5%AF%A6%E4%BD%9C%E5%90%84%E8%87%AA%E8%A9%B2%E5%AF%AB%E4%BB%80%E9%BA%BC/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E5%87%BD%E5%BC%8F%E6%96%87%E4%BB%B6%E5%88%86%E5%B1%A4%E8%A8%AD%E8%A8%88%E5%9E%8B%E5%88%A5%E4%BB%8B%E9%9D%A2%E5%AF%A6%E4%BD%9C%E5%90%84%E8%87%AA%E8%A9%B2%E5%AF%AB%E4%BB%80%E9%BA%BC/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：doc 是塑造使用者決策的工具——寫不好的 doc 會反向誤導使用者選錯路。
&lt;strong>設計原則&lt;/strong>：把資訊放在能表達它的最低層次（名稱 / 型別 / 介面 doc / 實作 doc / 範例與測試）、上層留給「下層表達不了的剩餘」。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點doc-是塑造使用者決策的工具">起點：doc 是塑造使用者決策的工具&lt;/h2>
&lt;p>API 設計者常忽略一件事：&lt;strong>文件本身會塑造使用者的決策&lt;/strong>——讀者依照 doc 給的資訊選預設值、選呼叫方式、選用途，所以 doc 寫不好就會反向誤導使用者選錯路。&lt;/p>
&lt;p>幾種常見的誤導模式：&lt;/p>
&lt;ul>
&lt;li>把「需要明確選擇」的東西做成「最少打字的預設」（例如某些 stream / channel API 預設是單訂閱、多數 SQL column 預設 nullable）——使用者讀不到「該選什麼」的資訊，跟著預設走就出包&lt;/li>
&lt;li>註解重複型別已說明的事，反而讓讀者懷疑「型別是不是不夠精確」&lt;/li>
&lt;li>介面 doc 描述「目前實作怎麼做」而非「契約承諾什麼」——讓未來新實作以為要照抄&lt;/li>
&lt;li>用憑想像的業務動機補完，後人讀了當真，反向影響其他相關決策&lt;/li>
&lt;/ul>
&lt;p>這些問題不是「沒寫 doc」，而是「&lt;strong>寫了誤導的 doc&lt;/strong>」。要寫出不誤導的 doc，得先想清楚每個位置該放什麼資訊。&lt;/p>
&lt;hr>
&lt;h2 id="設計原則資訊應該存在最低能表達它的層次">設計原則：資訊應該存在最低能表達它的層次&lt;/h2>
&lt;p>讀者讀一個 function 的閱讀順序：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>看簽章&lt;/strong>（名稱、參數、回傳型別）&lt;/li>
&lt;li>&lt;strong>讀 doc comment&lt;/strong>&lt;/li>
&lt;li>&lt;strong>跳進實作&lt;/strong>&lt;/li>
&lt;li>&lt;strong>找範例 / 測試&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>每往下一層，閱讀成本就高一級。設計 doc 的原則：&lt;/p>
&lt;blockquote>
&lt;p>能用上層表達的資訊，就不要往下層放。&lt;/p>&lt;/blockquote>
&lt;p>對應的職責劃分：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層次&lt;/th>
 &lt;th>該裝什麼&lt;/th>
 &lt;th>反例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>名稱&lt;/td>
 &lt;td>動詞 / 動作意圖&lt;/td>
 &lt;td>&lt;code>getData()&lt;/code>、&lt;code>process()&lt;/code>、&lt;code>handle()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>型別簽章&lt;/td>
 &lt;td>輸入合法範圍、回傳保證&lt;/td>
 &lt;td>&lt;code>int qty&lt;/code>（允許負數）、&lt;code>String?&lt;/code> 沒指明何時為 null&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>介面 doc&lt;/td>
 &lt;td>契約承諾、所有實作都要遵守的行為&lt;/td>
 &lt;td>描述當前實作流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實作 doc&lt;/td>
 &lt;td>實作特有的 invariant、bug workaround&lt;/td>
 &lt;td>重複介面契約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範例 / 測試&lt;/td>
 &lt;td>抽象描述失敗的複雜用法&lt;/td>
 &lt;td>取代正常 doc&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把資訊放在能表達它的最低層次，能讓上層 doc 更精簡、更精準。&lt;/p>
&lt;hr>
&lt;h2 id="layer-1名稱與型別簽章">Layer 1：名稱與型別簽章&lt;/h2>
&lt;p>&lt;strong>強型別語言下，型別是文件的一部分&lt;/strong>。很多 doc 內容本來就該由型別承擔。&lt;/p>
&lt;h3 id="用型別取代參數說明">用型別取代「參數說明」&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱：依賴 doc 警告
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// [quantity] 必須為正整數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：型別本身就限制
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PositiveInt&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱：String flag，靠 doc 說明可選值
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// [mode] 可選值：&amp;#39;manual&amp;#39;, &amp;#39;auto&amp;#39;, &amp;#39;hybrid&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">setMode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：用 enum
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">enum&lt;/span> &lt;span class="n">Mode&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">manual&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">auto&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">hybrid&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">setMode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Mode&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>當型別能表達約束時，&lt;strong>不要用 doc 重複表達&lt;/strong>——doc 是約束的弱形式（編譯不檢查、IDE 補全不提示），把 doc 當主要 enforcement 等於放棄型別系統的力氣。&lt;/p>
&lt;h3 id="用命名取代這個參數做什麼">用命名取代「這個參數做什麼」&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱：positional argument，靠 doc 解釋
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// [a] 是基準值，[b] 是新值
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：named argument 自說明
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="kd">required&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">from&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">required&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">to&lt;/span>&lt;span class="p">})&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>update(from: 5, to: 10)&lt;/code> 的呼叫端比 &lt;code>update(5, 10)&lt;/code> 清楚得多，且&lt;strong>不需要任何 doc&lt;/strong>。&lt;/p>
&lt;h3 id="用回傳型別表達失敗模式">用回傳型別表達失敗模式&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱：可能失敗，靠 doc 說「失敗時回傳 null」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 找不到時回傳 null
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">User&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：型別本身表達 optionality
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 更強：分清 null 跟 error
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">Result&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">NotFoundError&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>簽章已經表達清楚的事，doc 不必再寫。&lt;/p>
&lt;h3 id="命名要表達意圖不是實作">命名要表達意圖，不是實作&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱：implementation-leaking 命名
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Item&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getCachedItems&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：意圖命名
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">List&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Item&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getItems&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「Cached」這個字洩漏實作（用了 cache）。如果之後改成不 cache，名字就要改、所有 caller 也要改——但&lt;strong>業務語義並沒變&lt;/strong>。命名應該反映「呼叫者想要什麼」，不是「實作怎麼做」。&lt;/p>
&lt;blockquote>
&lt;p>展開閱讀：&lt;a href="../types-replacing-docs/">型別取代 doc 的收益曲線&lt;/a>——整理 null safety / enum / wrapper / Result / typestate 各自能消除哪類 doc、以及型別表達不了的剩餘部分（業務動機、性能、副作用、時序契約）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：doc 是塑造使用者決策的工具——寫不好的 doc 會反向誤導使用者選錯路。
<strong>設計原則</strong>：把資訊放在能表達它的最低層次（名稱 / 型別 / 介面 doc / 實作 doc / 範例與測試）、上層留給「下層表達不了的剩餘」。</p></blockquote>
<hr>
<h2 id="起點doc-是塑造使用者決策的工具">起點：doc 是塑造使用者決策的工具</h2>
<p>API 設計者常忽略一件事：<strong>文件本身會塑造使用者的決策</strong>——讀者依照 doc 給的資訊選預設值、選呼叫方式、選用途，所以 doc 寫不好就會反向誤導使用者選錯路。</p>
<p>幾種常見的誤導模式：</p>
<ul>
<li>把「需要明確選擇」的東西做成「最少打字的預設」（例如某些 stream / channel API 預設是單訂閱、多數 SQL column 預設 nullable）——使用者讀不到「該選什麼」的資訊，跟著預設走就出包</li>
<li>註解重複型別已說明的事，反而讓讀者懷疑「型別是不是不夠精確」</li>
<li>介面 doc 描述「目前實作怎麼做」而非「契約承諾什麼」——讓未來新實作以為要照抄</li>
<li>用憑想像的業務動機補完，後人讀了當真，反向影響其他相關決策</li>
</ul>
<p>這些問題不是「沒寫 doc」，而是「<strong>寫了誤導的 doc</strong>」。要寫出不誤導的 doc，得先想清楚每個位置該放什麼資訊。</p>
<hr>
<h2 id="設計原則資訊應該存在最低能表達它的層次">設計原則：資訊應該存在最低能表達它的層次</h2>
<p>讀者讀一個 function 的閱讀順序：</p>
<ol>
<li><strong>看簽章</strong>（名稱、參數、回傳型別）</li>
<li><strong>讀 doc comment</strong></li>
<li><strong>跳進實作</strong></li>
<li><strong>找範例 / 測試</strong></li>
</ol>
<p>每往下一層，閱讀成本就高一級。設計 doc 的原則：</p>
<blockquote>
<p>能用上層表達的資訊，就不要往下層放。</p></blockquote>
<p>對應的職責劃分：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>該裝什麼</th>
          <th>反例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>名稱</td>
          <td>動詞 / 動作意圖</td>
          <td><code>getData()</code>、<code>process()</code>、<code>handle()</code></td>
      </tr>
      <tr>
          <td>型別簽章</td>
          <td>輸入合法範圍、回傳保證</td>
          <td><code>int qty</code>（允許負數）、<code>String?</code> 沒指明何時為 null</td>
      </tr>
      <tr>
          <td>介面 doc</td>
          <td>契約承諾、所有實作都要遵守的行為</td>
          <td>描述當前實作流程</td>
      </tr>
      <tr>
          <td>實作 doc</td>
          <td>實作特有的 invariant、bug workaround</td>
          <td>重複介面契約</td>
      </tr>
      <tr>
          <td>範例 / 測試</td>
          <td>抽象描述失敗的複雜用法</td>
          <td>取代正常 doc</td>
      </tr>
  </tbody>
</table>
<p>把資訊放在能表達它的最低層次，能讓上層 doc 更精簡、更精準。</p>
<hr>
<h2 id="layer-1名稱與型別簽章">Layer 1：名稱與型別簽章</h2>
<p><strong>強型別語言下，型別是文件的一部分</strong>。很多 doc 內容本來就該由型別承擔。</p>
<h3 id="用型別取代參數說明">用型別取代「參數說明」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：依賴 doc 警告
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [quantity] 必須為正整數
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：型別本身就限制
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：String flag，靠 doc 說明可選值
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [mode] 可選值：&#39;manual&#39;, &#39;auto&#39;, &#39;hybrid&#39;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="kt">String</span> <span class="n">mode</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：用 enum
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">enum</span> <span class="n">Mode</span> <span class="p">{</span> <span class="n">manual</span><span class="p">,</span> <span class="n">auto</span><span class="p">,</span> <span class="n">hybrid</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="n">Mode</span> <span class="n">mode</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>當型別能表達約束時，<strong>不要用 doc 重複表達</strong>——doc 是約束的弱形式（編譯不檢查、IDE 補全不提示），把 doc 當主要 enforcement 等於放棄型別系統的力氣。</p>
<h3 id="用命名取代這個參數做什麼">用命名取代「這個參數做什麼」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：positional argument，靠 doc 解釋
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [a] 是基準值，[b] 是新值
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：named argument 自說明
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">update</span><span class="p">({</span><span class="kd">required</span> <span class="kt">int</span> <span class="n">from</span><span class="p">,</span> <span class="kd">required</span> <span class="kt">int</span> <span class="n">to</span><span class="p">})</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p><code>update(from: 5, to: 10)</code> 的呼叫端比 <code>update(5, 10)</code> 清楚得多，且<strong>不需要任何 doc</strong>。</p>
<h3 id="用回傳型別表達失敗模式">用回傳型別表達失敗模式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：可能失敗，靠 doc 說「失敗時回傳 null」
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 找不到時回傳 null
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">User</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：型別本身表達 optionality
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">// 更強：分清 null 跟 error
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="n">Result</span><span class="o">&lt;</span><span class="n">User</span><span class="p">,</span> <span class="n">NotFoundError</span><span class="o">&gt;</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>簽章已經表達清楚的事，doc 不必再寫。</p>
<h3 id="命名要表達意圖不是實作">命名要表達意圖，不是實作</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：implementation-leaking 命名
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getCachedItems</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 強：意圖命名
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>「Cached」這個字洩漏實作（用了 cache）。如果之後改成不 cache，名字就要改、所有 caller 也要改——但<strong>業務語義並沒變</strong>。命名應該反映「呼叫者想要什麼」，不是「實作怎麼做」。</p>
<blockquote>
<p>展開閱讀：<a href="../types-replacing-docs/">型別取代 doc 的收益曲線</a>——整理 null safety / enum / wrapper / Result / typestate 各自能消除哪類 doc、以及型別表達不了的剩餘部分（業務動機、性能、副作用、時序契約）。</p></blockquote>
<hr>
<h2 id="layer-2介面-doc">Layer 2：介面 doc</h2>
<p>介面 doc 是<strong>契約</strong>（contract）——對所有實作的承諾。它的讀者有兩類：</p>
<ol>
<li><strong>使用者</strong>：「我呼叫這個會發生什麼？需要注意什麼？」</li>
<li><strong>實作者</strong>（包括寫 mock、寫新版實作的人）：「我必須遵守哪些規則？」</li>
</ol>
<p>兩類讀者都不該為了讀懂契約而去讀任何單一實作。</p>
<h3 id="該寫的契約承諾行為保證隱性需求">該寫的：契約承諾、行為保證、隱性需求</h3>
<ul>
<li><strong>何時 throw / 回傳特殊值</strong>：「找不到時 throw <code>NotFoundException</code>」</li>
<li><strong>副作用</strong>：「呼叫後 <code>currentUser</code> 會被清空」</li>
<li><strong>同步 / 非同步保證</strong>：「呼叫後資料庫立即一致；快取要等下一次 refresh」</li>
<li><strong>執行順序保證</strong>：「listener 觸發順序不保證」</li>
<li><strong>業務規則</strong>（<strong>只在有實際業務需求時寫，且要有來源</strong>）：「會員價只能用 wallet 付款」</li>
</ul>
<h3 id="容易誤入介面-doc-的內容屬於型別實作或他處">容易誤入介面 doc 的內容（屬於型別、實作或他處）</h3>
<p>介面 doc 的職責是<strong>契約描述</strong>——所以「型別簽章已說的事」「特定實作怎麼做」「沒來源的業務動機」分屬其他層次（型別、實作 doc、issue tracker）、寫進介面 doc 反而稀釋契約本身的能見度。三個典型誤入：</p>
<h4 id="1-型別已表達的內容屬於型別簽章">1. 型別已表達的內容（屬於型別簽章）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 冗：
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 回傳 User，找不到時為 null
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 簡：型別已說明，doc 留白或寫業務動機
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span></span></span></code></pre></div><h4 id="2-當前實作的細節屬於實作-doc">2. 當前實作的細節（屬於實作 doc）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 冗：洩漏實作
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 內部用 HashMap 存儲，O(1) 查詢
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 簡：純契約
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span></span></span></code></pre></div><p>實作細節寫在介面 doc 會誤導實作者「這個契約規定要用 HashMap」。如果未來有人寫一個用 B-tree 的實作，是合法的，但讀 doc 會以為違反契約。</p>
<h4 id="3-憑想像補完的業務動機屬於-issue-tracker--不寫">3. 憑想像補完的業務動機（屬於 issue tracker / 不寫）</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 冗（且可能錯）：
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 為了符合 PCI-DSS 規範，這裡不能 log 完整 cardNumber
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">String</span> <span class="n">maskCardNumber</span><span class="p">(</span><span class="kt">String</span> <span class="n">cardNumber</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 簡（沒來源就只寫可觀察事實）：
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">/// 回傳遮罩後字串，僅保留尾 4 碼
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="kt">String</span> <span class="n">maskCardNumber</span><span class="p">(</span><span class="kt">String</span> <span class="n">cardNumber</span><span class="p">);</span></span></span></code></pre></div><p>業務動機要有來源（規範文件、PM 決策、incident 紀錄）才寫；猜的不要寫。猜的動機被當真會反向影響後續決策——讀者拿這條沒來源的猜測當依據、推到「既然是因為 PCI-DSS、那 X 也要這樣處理」、就把錯誤論述擴散到下游。</p>
<h3 id="介面-doc-越精簡越能被讀完">介面 doc 越精簡越能被讀完</h3>
<p>很多人覺得「寫得詳細才負責任」，結果介面 doc 三段五行，讀完也記不住。<strong>好的介面 doc 通常只有 2-4 行</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 從本地購物車移除指定商品
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">///
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">/// 找不到對應品項時不做事；不會拋例外。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">);</span></span></span></code></pre></div><p>第一行說 what、第二行說 edge case。寫到這就停。「指定商品」怎麼比對？無關契約，去看實作。</p>
<hr>
<h2 id="layer-3實作-doc">Layer 3：實作 doc</h2>
<p>實作 doc 的職責跟介面 doc<strong>完全不同</strong>：</p>
<ul>
<li><strong>介面 doc</strong>：對外契約，所有實作共通</li>
<li><strong>實作 doc</strong>：這個實作特有的細節</li>
</ul>
<h3 id="該寫的實作特有的-invariantworkaroundtradeoff">該寫的：實作特有的 invariant、workaround、tradeoff</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 該寫：實作特有的 invariant
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kt">void</span> <span class="n">increaseItemQuantity</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="c1">// 順序關鍵：先 set lastChangedItem 再動 list，
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="c1">// 因為訂閱 localCartItems 的 worker 會在 list 變動時讀 lastChangedItem
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="n">lastChangedItem</span><span class="p">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">item</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="n">localCartItems</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">=</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 該寫：bug workaround
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// Workaround for SQLite issue #1234: integer overflow on 32-bit Android,
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">// 拆成兩步 query 避開
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">ids</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">db</span><span class="p">.</span><span class="n">rawQuery</span><span class="p">(</span><span class="s1">&#39;SELECT id FROM ...&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">return</span> <span class="kd">await</span> <span class="n">db</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="s1">&#39;items&#39;</span><span class="p">,</span> <span class="nl">where:</span> <span class="s1">&#39;id IN (</span><span class="si">${</span><span class="n">ids</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;,&#34;</span><span class="p">)</span><span class="si">}</span><span class="s1">)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">// 該寫：性能 tradeoff
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">// 用 LinkedHashMap 而非普通 Map：插入 1k 次後查詢效能差 3-5 倍
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">cache</span> <span class="o">=</span> <span class="n">LinkedHashMap</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="n">Item</span><span class="o">&gt;</span><span class="p">();</span></span></span></code></pre></div><p>這些都是**讀實作 code 也看不出「為什麼要這樣」**的決定，需要 doc 解釋。</p>
<h3 id="契約只寫一處實作不重複介面已寫的規則">契約只寫一處：實作不重複介面已寫的規則</h3>
<p>實作 doc 的職責跟介面 doc 互補——契約描述歸介面層、實作層只補「該實作的特殊性」。同一條契約規則寫第二次（在實作層複述介面已寫的承諾）會破壞「契約只寫一次」原則：規則改的時候要同步兩處、少改一處就出現自相矛盾的文件、讀者看到也分不清以哪份為準。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 不該寫：介面 doc 已寫的規則，實作不再重複
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// 移除不視為「最後變更」，不更新 lastChangedItem
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="n">localCartItems</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>「移除不更新 lastChangedItem」是契約、介面層已寫。</p>
<p>如果擔心未來維護者誤以為「作者忘了寫」，留一個<strong>指向介面</strong>的最小提示比複述整條規則更安全：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="err">@</span><span class="n">override</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 行為見 ICartService.removeFromLocalCart
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">removeFromLocalCart</span><span class="p">(</span><span class="n">CartItem</span> <span class="n">item</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">localCartItems</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>不重複規則，只指向真相來源。</p>
<h3 id="negative-space-documentation">Negative-space documentation</h3>
<p>實作 doc 偶爾要寫「<strong>為什麼這裡刻意沒寫某段程式</strong>」。這類 doc 防的是「未來維護者順手補上」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kt">void</span> <span class="n">processPayment</span><span class="p">(</span><span class="n">Payment</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// NOTE: 這裡刻意不 retry —— payment gateway 是非冪等，
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="c1">// retry 會造成重複扣款。失敗一律拋給上層人工處理。
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="k">return</span> <span class="n">_gateway</span><span class="p">.</span><span class="n">charge</span><span class="p">(</span><span class="n">p</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>沒這條註解，下個維護者看到網路 retry 是常見做法，可能會「順手加上」造成事故。</p>
<p>negative-space doc 用得好可以避免事故；用得多會變成處處防禦性註解，閱讀體驗變差。原則：<strong>這個「刻意沒做」的決定，是不是違反讀者的合理直覺？</strong> 違反才寫。</p>
<hr>
<h2 id="layer-4範例與測試">Layer 4：範例與測試</h2>
<p>複雜 API 的最後一層 doc 是<strong>可執行範例</strong>。</p>
<p>何時用 example：</p>
<ul>
<li>API 有多個正交參數，組合起來很多種用法</li>
<li>抽象描述比看程式碼難懂</li>
<li>邊界 case 用文字描述模糊（「如果 collection 是空、且 timeout 為 zero、且 retries 為 0…」）</li>
</ul>
<p>何時不用 example：</p>
<ul>
<li>API 用法只有一種、簽章已說清</li>
<li>用法跟名稱字面意義一致</li>
</ul>
<p><strong>測試也是 doc</strong>。命名好的測試比 example 更有價值——不會 outdated（測試會跑、example 不會），且涵蓋 edge case。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns null when item not in cart&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;decreases quantity when item exists with quantity &gt; 1&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;removes item when quantity reaches 0&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>讀者看 function 不確定行為時，<strong>跳到對應 test file 比讀冗長 doc 快</strong>——測試案例的命名直接告訴你支援哪些 case，並且每個案例都有可執行的具體輸入輸出。</p>
<blockquote>
<p>展開閱讀：<a href="../test-naming-as-documentation/">測試命名作為文件</a>——測試是少數會自我驗證的文件、把命名寫成可執行 spec 條目就能取代不少 doc 的職責。</p></blockquote>
<hr>
<h2 id="常見反模式">常見反模式</h2>
<h3 id="反模式-1用-doc-取代不好的命名">反模式 1：用 doc 取代不好的命名</h3>
<p><strong>正向概念</strong>：命名是契約的最強形式、doc 是命名表達不了的剩餘部分的家。命名先到位、doc 才有空間寫真正重要的事。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：靠 doc 補救命名
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 處理訂單，但只在訂單狀態為 pending 時做事
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handle</span><span class="p">(</span><span class="n">Order</span> <span class="n">o</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 正：命名表達意圖
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handlePendingOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">o</span><span class="p">);</span></span></span></code></pre></div><p>把 doc 當成命名失敗的補丁有兩個問題：(1)「需要讀 doc 才能用對」的 function 在 IDE 自動補全 / 快速瀏覽時看不到 doc、誤用機率高；(2) 命名其實沒變、別人改 code 時 doc 會跟不上、補丁本身又 outdated。「需要 doc 才能用對」通常是命名沒到位的訊號。</p>
<h3 id="反模式-2過度註解">反模式 2：過度註解</h3>
<p><strong>正向概念</strong>：doc 是稀缺資源——讀者注意力的預算有限、把 doc 留給「值得花注意力讀」的事項。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 反：句句都是 noise
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c1">/// User 的 ID
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">id</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">/// User 的名字
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">/// User 的 email
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// 正：欄位名清楚就不寫
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="kt">String</span> <span class="n">id</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="kt">String</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>「<code>User.name</code> 是 User 的名字」屬於命名已表達的訊息、寫進 doc 只是 redundant noise。整份 code 充斥這類 doc 會稀釋訊號——讀者習慣性 skip 所有 doc 之後、連真正重要的 invariant 跟 edge case 也會被一起跳過。</p>
<h3 id="反模式-3過去式-doc">反模式 3：過去式 doc</h3>
<p><strong>正向概念</strong>：source code doc 描述「<strong>現在</strong>這份 code 在做什麼」、commit message 描述「<strong>那一刻</strong>為什麼要改」。兩種讀者要找的資訊不同、各歸各的家。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：寫給歷史
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// 修了 issue #123 的 race condition
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 正：寫給未來讀者（保留 fix 的關鍵 invariant 即可）
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">process</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="c1">// 必須在持有 lock 內 call observer，避免 observer 看到中間狀態
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>  <span class="p">...</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>「修了什麼 bug」凍結在過去某一刻、屬於 commit message / changelog；「目前必須持有 lock」是契約限制、屬於 source code doc。把過去式直接塞進 source 等於用 source 重做一份 git log——但 git log 已經存在、且結構化、可搜尋、有 author / timestamp。</p>
<blockquote>
<p>展開閱讀：<a href="../commit-message-vs-source-doc/">Commit message vs source code doc</a>——時序敏感的資訊（為什麼這次改、考慮過什麼方案）放 commit、持續適用的契約放 source、配合 git blame 工作流讓考古路徑清楚。</p></blockquote>
<h3 id="反模式-4同一條規則多處寫">反模式 4：同一條規則多處寫</h3>
<p><strong>正向概念</strong>：契約由介面層獨家承載、其他層引用即可。規則只有一個 SSoT（Single Source of Truth）、修改成本才可控。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：規則寫三處
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 介面：「取消訂單後 3 天內不能重新下單」
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// 實作：「取消後 3 天內不能重新下單」
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 測試：「驗證取消後 3 天內不能重新下單」
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// 正：規則寫一處（介面），其他指向
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">// 介面：「取消訂單後 3 天內不能重新下單」
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">// 實作：（無 doc）
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="o">//</span> <span class="err">測試：</span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;cannot reorder within 3 days of cancellation&#39;</span><span class="p">)</span></span></span></code></pre></div><p>一條規則複製到三處看起來保險、但會在改規則時暴露代價：要同步修三處、漏改一處就出現自相矛盾的 doc、讀者讀到不一致的版本反而會懷疑「以哪份為準」。把規則收斂到單一介面、其他層指向（測試命名 / 實作註解 <code>// 行為見 ...</code>）就夠了。</p>
<h3 id="反模式-5把語法選擇當成-doc-內容">反模式 5：把語法選擇當成 doc 內容</h3>
<p><strong>正向概念</strong>：doc 描述業務目的跟行為契約——讀者要的是「這個 function 做什麼」、不是「為什麼用這個語法寫」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 反：寫實作層次的選擇細節
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 用 Dart 3 的 record pattern destructure，比 .$1 / .$2 可讀
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handle</span><span class="p">((</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="n">event</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">final</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="o">=</span> <span class="n">event</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">...</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 正：寫業務動機 / 行為契約
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">/// 處理 (timestamp, value) 對的批次更新
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">handle</span><span class="p">((</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="n">event</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>「為什麼用某語法」屬於 commit message / PR review 的討論記錄、不屬於 source code doc——換個語法寫法、業務行為沒變、但 doc 卻會 outdated。語法選擇的 why 在 git log / PR description 找得到、不需要 source 背這份歷史。</p>
<h3 id="反模式-6用-doc-警告使用者請別這樣用">反模式 6：用 doc 警告使用者「請別這樣用」</h3>
<p><strong>正向概念</strong>：能用型別 / API 設計禁掉的誤用、把它編進型別系統；doc 警告留給型別表達不了的使用情境（時序、跨方法 invariant、執行環境）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：靠 doc 警告
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// **不要**直接修改回傳的 list，會造成內部狀態不一致
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 正：型別 / API 設計阻止誤用
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">List</span><span class="p">.</span><span class="n">unmodifiable</span><span class="p">(</span><span class="n">_items</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="o">//</span> <span class="err">或回傳</span> <span class="n">Iterable</span> <span class="o">/</span> <span class="n">immutable</span> <span class="err">集合型別</span></span></span></code></pre></div><p>doc 警告的執行力靠使用者「願意讀並且記住」、型別約束則是編譯期強制——當失敗成本高（內部狀態被破壞）、保護機制就值得從 doc 升到型別。型別表達不了的使用情境（例如「必須在 main isolate 呼叫」）才是 doc 警告該守的範圍。</p>
<hr>
<h2 id="api-設計層面doc-之外的塑造工具">API 設計層面：doc 之外的塑造工具</h2>
<p>doc 寫得再好，<strong>API 設計本身</strong>會更直接塑造使用者行為。要讓使用者選對，從設計層下手比寫 doc 有效。</p>
<h3 id="預設值要選多數情況下對的">預設值要選「多數情況下對的」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 預設導向受限選項：使用者忘了選通用版本就出錯
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">StreamController</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">ctrl</span> <span class="o">=</span> <span class="n">StreamController</span><span class="p">();</span>  <span class="c1">// single
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 預設導向通用選項：忘了選受限版本不會出錯
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">StreamController</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">ctrl</span> <span class="o">=</span> <span class="n">StreamController</span><span class="p">.</span><span class="n">broadcast</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="o">//</span> <span class="err">受限版本要顯式選</span> <span class="p">.</span><span class="n">singleSubscription</span><span class="p">()</span></span></span></code></pre></div><p>當預設造成的失敗成本高、失敗模式又不易察覺、把多數人實際需要的選項變成預設、能消除整類「忘了選」的事故。doc 警告的執行力靠「使用者讀到並記住」、規模一大就守不住——把保護從約定升到結構。</p>
<h3 id="把選擇從-default-取消用型別禁掉">把選擇從 default 取消（用型別禁掉）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：靠 doc 說「不該直接呼叫，請用 X」
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="err">@</span><span class="n">protected</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kt">void</span> <span class="n">internalMethod</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：型別系統禁掉
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">_InternalImpl</span> <span class="p">{</span> <span class="kt">void</span> <span class="n">method</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="p">}</span></span></span></code></pre></div><p>能用 visibility / sealed / private 收掉的「請別這樣用」、把它收進型別系統——比起 doc 提示、語言層級的禁用是無條件強制的、且不會在大型重構時被遺漏。</p>
<h3 id="builder--fluent-api-取代多參數">Builder / fluent API 取代多參數</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：positional / named 多參數，靠 doc 解釋
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">Request</span> <span class="n">build</span><span class="p">(</span><span class="kt">String</span> <span class="n">url</span><span class="p">,</span> <span class="p">[</span><span class="n">Map</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;?</span> <span class="n">headers</span><span class="p">,</span> <span class="n">Body</span><span class="o">?</span> <span class="n">body</span><span class="p">,</span> <span class="kt">int</span> <span class="n">timeout</span> <span class="o">=</span> <span class="m">30</span><span class="p">]);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 強：fluent API 自說明
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">Request</span><span class="p">.</span><span class="n">builder</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">.</span><span class="n">header</span><span class="p">(</span><span class="s1">&#39;Accept&#39;</span><span class="p">,</span> <span class="s1">&#39;json&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">.</span><span class="n">body</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">.</span><span class="n">timeout</span><span class="p">(</span><span class="n">Duration</span><span class="p">(</span><span class="nl">seconds:</span> <span class="m">30</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">  <span class="p">.</span><span class="n">build</span><span class="p">();</span></span></span></code></pre></div><p>fluent API 的 method 名直接表達意圖，不需要 doc 解釋每個參數做什麼。</p>
<hr>
<h2 id="寫-function-doc-的-checklist">寫 function doc 的 checklist</h2>
<p>寫一個 function doc 前，跑這個 checklist：</p>
<ul>
<li><input disabled="" type="checkbox"> <strong>這條資訊型別能不能表達？</strong> 能 → 改 type，不寫 doc</li>
<li><input disabled="" type="checkbox"> <strong>這條資訊命名能不能表達？</strong> 能 → 改名，不寫 doc</li>
<li><input disabled="" type="checkbox"> <strong>這條資訊是契約還是實作細節？</strong> 契約 → 介面 doc / 實作 → 實作 doc</li>
<li><input disabled="" type="checkbox"> <strong>這條規則是不是已經寫在介面 doc？</strong> 是 → 實作不重複</li>
<li><input disabled="" type="checkbox"> <strong>這個業務動機有沒有來源？</strong> 沒有 → 不寫，只寫可觀察事實</li>
<li><input disabled="" type="checkbox"> <strong>這個 doc 在描述什麼時候出問題？</strong> 是 → 寫得明確（throw / null / edge case）</li>
<li><input disabled="" type="checkbox"> <strong>沒有這條 doc，讀者會誤判嗎？</strong> 不會 → 不寫</li>
<li><input disabled="" type="checkbox"> <strong>同一條規則我寫了第二次嗎？</strong> 是 → 砍一處，留一處</li>
</ul>
<p>過完 checklist 留下的 doc 通常很短——<strong>這是好現象</strong>。</p>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>doc 是「<strong>型別、簽章、命名、結構都表達不了的剩餘資訊</strong>」的家。</p></blockquote>
<p>寫 doc 之前先問：</p>
<ul>
<li>能用型別表達嗎？</li>
<li>能用命名表達嗎？</li>
<li>能用結構（fluent API、enum、sealed class）表達嗎？</li>
</ul>
<p>三題都答「不能」、<strong>而且</strong>使用者不知道會出錯——這時才需要 doc。</p>
<p>這個原則的 corollary：<strong>型別系統越強的語言、function doc 也越能寫得短</strong>。如果發現 Dart / TypeScript / Rust 的 function doc 寫得跟 Python 一樣長、多半有東西可以下移到型別。</p>
<h3 id="何時-doc-還是該寫得詳細">何時 doc 還是該寫得詳細</h3>
<p>「能少寫就少寫」是預設、<strong>但有些情境 doc 必須寫得詳細</strong>——這些是型別跟結構覆蓋不到的場景：</p>
<ul>
<li><strong>跨方法 protocol</strong>：「呼叫 <code>reserve</code> 之後必須在 X 內呼叫 <code>commit</code> 或 <code>release</code>」——typestate 能部分表達但寫法繁瑣、多數情況靠 doc 是合理的</li>
<li><strong>時序契約</strong>：「寫入後最多 1 秒內 read replica 可見」「retry 5 次後放棄」——跨呼叫、跨時間的契約、型別表達不了</li>
<li><strong>副作用 / 對外部系統的影響</strong>：「會寫入 audit log」「會發 webhook」——caller 需要知道才能規劃整體流程</li>
<li><strong>業務規則 + 有來源</strong>：「會員價只能用 wallet 付款（業務需求 #1234）」——有出處的業務動機要寫、避免後人誤刪</li>
<li><strong>效能契約</strong>：「O(log n) 查詢；不適合在熱迴圈呼叫」——caller 要根據這個資訊選用法</li>
</ul>
<p>「短」不是目標、「精準」才是。把該下移的下移到型別、剩下的就值得詳細寫。</p>
<hr>
<h2 id="收束doc-設計就是-api-設計">收束：doc 設計就是 API 設計</h2>
<p>回到開頭——doc 寫不好會誤導使用者。但更深一層的觀察是：<strong>「需要寫很多 doc 才能用對」本身就是 API 設計的紅旗</strong>。</p>
<p>好的 API 用最少的 doc 就能讓使用者用對：</p>
<ul>
<li>命名直接表達意圖</li>
<li>型別表達合法輸入與失敗模式</li>
<li>結構（enum、sealed、builder）防止誤用</li>
<li>預設值導向多數情況下正確的選擇</li>
<li>殘餘的契約與 edge case 用簡短介面 doc 說明</li>
<li>實作特有的 invariant 用簡短實作註解說明</li>
</ul>
<p>寫 doc 的時候同時問「<strong>這條 doc 想說的事，是不是該由 API 設計本身承擔？</strong>」——這個問題能讓你的 doc 跟 API 同時變更好。</p>
]]></content:encoded></item><item><title>型別取代 doc 的收益曲線：強型別語言的 doc 該有多短</title><link>https://tarrragon.github.io/blog/record/%E5%9E%8B%E5%88%A5%E5%8F%96%E4%BB%A3-doc-%E7%9A%84%E6%94%B6%E7%9B%8A%E6%9B%B2%E7%B7%9A%E5%BC%B7%E5%9E%8B%E5%88%A5%E8%AA%9E%E8%A8%80%E7%9A%84-doc-%E8%A9%B2%E6%9C%89%E5%A4%9A%E7%9F%AD/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E5%9E%8B%E5%88%A5%E5%8F%96%E4%BB%A3-doc-%E7%9A%84%E6%94%B6%E7%9B%8A%E6%9B%B2%E7%B7%9A%E5%BC%B7%E5%9E%8B%E5%88%A5%E8%AA%9E%E8%A8%80%E7%9A%84-doc-%E8%A9%B2%E6%9C%89%E5%A4%9A%E7%9F%AD/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：型別系統強化等於 doc 表達力轉移——很多 doc 內容應該下移到型別。
&lt;strong>設計原則&lt;/strong>：能用型別表達的限制，不要用 doc 表達；doc 是型別表達不了的剩餘資訊的家。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../function-doc-layered-design/">函式文件分層設計&lt;/a> 的 Layer 1（名稱與型別簽章）展開——把「型別承擔哪些原本寫在 doc 的內容」拉成獨立主題討論。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點型別越強doc-的職責範圍就越窄">起點：型別越強、doc 的職責範圍就越窄&lt;/h2>
&lt;p>「型別系統越強、function doc 也越能寫得短」——這是個普遍但不被刻意利用的現象。&lt;/p>
&lt;p>當你看到一個 Dart / TypeScript / Rust 的 function doc 寫得跟 Python / JavaScript 一樣長、多半有東西可以下移到型別。把可下移的內容下移、doc 表面變短、實質上的好處更深：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>編譯期被檢查&lt;/strong>——型別說的事不會 outdated（doc 會）&lt;/li>
&lt;li>&lt;strong>IDE 補全提示&lt;/strong>——使用者看到型別就懂、不用切到文件頁&lt;/li>
&lt;li>&lt;strong>重構時連動&lt;/strong>——改型別會逼所有 caller 跟著改、doc 改了沒人逼你檢查&lt;/li>
&lt;/ul>
&lt;p>這篇整理：哪些常見的 doc 內容能被型別取代、哪些下移了會破壞別的東西、以及型別越加越強時要怎麼平衡 ergonomic 跟表達力。&lt;/p>
&lt;hr>
&lt;h2 id="可被型別取代的常見-doc-內容">可被型別取代的常見 doc 內容&lt;/h2>
&lt;p>下面 8 類 doc 內容、共通特徵是「可以從 doc 約定升級成型別約束」——升級之後、保護從「靠使用者讀並記住」變成「靠編譯器強制」、執行力跟一致性都比 doc 強。每類列出弱（doc 約定）vs 強（型別約束）的對比。&lt;/p>
&lt;h3 id="1-必須是正整數必須非空必須在範圍內">1. 「必須是正整數」「必須非空」「必須在範圍內」&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱：依賴 doc 警告
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// [quantity] 必須為正整數（&amp;gt;= 1）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">quantity&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">throw&lt;/span> &lt;span class="n">ArgumentError&lt;/span>&lt;span class="p">(...);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：refinement type / value object
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">PositiveInt&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">value&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="n">PositiveInt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">throw&lt;/span> &lt;span class="n">ArgumentError&lt;/span>&lt;span class="p">(...);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">PositiveInt&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1">// 最強（語言支援的話）：refinement types
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">void&lt;/span> &lt;span class="n">increase&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">int&lt;/span> &lt;span class="n">quantity&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="n">where&lt;/span> &lt;span class="n">quantity&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Dart 沒有 native refinement type，但用 wrapper class 一樣能達到「&lt;strong>呼叫端要顯式建構合法值才能呼叫&lt;/strong>」的效果。validation 從「呼叫進入 function 後才檢查」前移到「建構 value object 時檢查」，contract 變成型別系統的一部分。&lt;/p>
&lt;h3 id="2-可能為-null找不到時回傳-null">2. 「可能為 null」「找不到時回傳 null」&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱（前 null safety 時代）：
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// [name] 可為 null，[email] 不可為 null
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kt">String&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kt">String&lt;/span> &lt;span class="n">email&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 找不到時回傳 null
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">User&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強（null safety）：
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">User&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kt">String&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 型別已說可為 null
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">String&lt;/span> &lt;span class="n">email&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 型別已說不可為 null
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="n">User&lt;/span>&lt;span class="o">?&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="err">型別已說可能找不到&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Dart / TypeScript / Kotlin / Swift 的 sound null safety 把「可為 null」從 doc 約定升級成型別約定——升級之後、「[X] 可為 null」這類 doc 變成 redundant noise（型別已經精準說了、重複寫只是稀釋訊號、改型別時忘了同步 doc 還會誤導讀者）。&lt;/p>
&lt;h3 id="3-會-throw-某-exception">3. 「會 throw 某 exception」&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 弱：靠 doc
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 找不到時 throw [NotFoundException]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 網路錯誤時 throw [NetworkException]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">Future&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">&amp;gt;&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">// 強：用 Result / Either / sealed class
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">Future&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">Result&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">GetUserError&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kt">String&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n">sealed&lt;/span> &lt;span class="kd">class&lt;/span> &lt;span class="nc">GetUserError&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">NotFoundError&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">GetUserError&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="kd">class&lt;/span> &lt;span class="nc">NetworkError&lt;/span> &lt;span class="kd">extends&lt;/span> &lt;span class="n">GetUserError&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">statusCode&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Result / Either pattern 把 error 從「invisible exception」升級成「型別簽章可見的回傳值」。Caller 必須處理（編譯不過 if not handled），不會漏掉 error path。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：型別系統強化等於 doc 表達力轉移——很多 doc 內容應該下移到型別。
<strong>設計原則</strong>：能用型別表達的限制，不要用 doc 表達；doc 是型別表達不了的剩餘資訊的家。</p></blockquote>
<blockquote>
<p>本篇是 <a href="../function-doc-layered-design/">函式文件分層設計</a> 的 Layer 1（名稱與型別簽章）展開——把「型別承擔哪些原本寫在 doc 的內容」拉成獨立主題討論。</p></blockquote>
<hr>
<h2 id="起點型別越強doc-的職責範圍就越窄">起點：型別越強、doc 的職責範圍就越窄</h2>
<p>「型別系統越強、function doc 也越能寫得短」——這是個普遍但不被刻意利用的現象。</p>
<p>當你看到一個 Dart / TypeScript / Rust 的 function doc 寫得跟 Python / JavaScript 一樣長、多半有東西可以下移到型別。把可下移的內容下移、doc 表面變短、實質上的好處更深：</p>
<ul>
<li><strong>編譯期被檢查</strong>——型別說的事不會 outdated（doc 會）</li>
<li><strong>IDE 補全提示</strong>——使用者看到型別就懂、不用切到文件頁</li>
<li><strong>重構時連動</strong>——改型別會逼所有 caller 跟著改、doc 改了沒人逼你檢查</li>
</ul>
<p>這篇整理：哪些常見的 doc 內容能被型別取代、哪些下移了會破壞別的東西、以及型別越加越強時要怎麼平衡 ergonomic 跟表達力。</p>
<hr>
<h2 id="可被型別取代的常見-doc-內容">可被型別取代的常見 doc 內容</h2>
<p>下面 8 類 doc 內容、共通特徵是「可以從 doc 約定升級成型別約束」——升級之後、保護從「靠使用者讀並記住」變成「靠編譯器強制」、執行力跟一致性都比 doc 強。每類列出弱（doc 約定）vs 強（型別約束）的對比。</p>
<h3 id="1-必須是正整數必須非空必須在範圍內">1. 「必須是正整數」「必須非空」「必須在範圍內」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 弱：依賴 doc 警告
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// [quantity] 必須為正整數（&gt;= 1）
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">quantity</span> <span class="o">&lt;</span> <span class="m">1</span><span class="p">)</span> <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(...);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 強：refinement type / value object
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">PositiveInt</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="kd">final</span> <span class="kt">int</span> <span class="n">value</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="n">PositiveInt</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="o">&lt;</span> <span class="m">1</span><span class="p">)</span> <span class="k">throw</span> <span class="n">ArgumentError</span><span class="p">(...);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">quantity</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1">// 最強（語言支援的話）：refinement types
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">increase</span><span class="p">(</span><span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span> <span class="n">where</span> <span class="n">quantity</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>Dart 沒有 native refinement type，但用 wrapper class 一樣能達到「<strong>呼叫端要顯式建構合法值才能呼叫</strong>」的效果。validation 從「呼叫進入 function 後才檢查」前移到「建構 value object 時檢查」，contract 變成型別系統的一部分。</p>
<h3 id="2-可能為-null找不到時回傳-null">2. 「可能為 null」「找不到時回傳 null」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 弱（前 null safety 時代）：
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// [name] 可為 null，[email] 不可為 null
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kt">String</span><span class="o">?</span> <span class="n">name</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">/// 找不到時回傳 null
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="n">User</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 強（null safety）：
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kt">String</span><span class="o">?</span> <span class="n">name</span><span class="p">;</span>       <span class="c1">// 型別已說可為 null
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="kt">String</span> <span class="n">email</span><span class="p">;</span>       <span class="c1">// 型別已說不可為 null
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">User</span><span class="o">?</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>  <span class="o">//</span> <span class="err">型別已說可能找不到</span></span></span></code></pre></div><p>Dart / TypeScript / Kotlin / Swift 的 sound null safety 把「可為 null」從 doc 約定升級成型別約定——升級之後、「[X] 可為 null」這類 doc 變成 redundant noise（型別已經精準說了、重複寫只是稀釋訊號、改型別時忘了同步 doc 還會誤導讀者）。</p>
<h3 id="3-會-throw-某-exception">3. 「會 throw 某 exception」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 弱：靠 doc
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 找不到時 throw [NotFoundException]
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 網路錯誤時 throw [NetworkException]
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">User</span><span class="o">&gt;</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 強：用 Result / Either / sealed class
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="n">Result</span><span class="o">&lt;</span><span class="n">User</span><span class="p">,</span> <span class="n">GetUserError</span><span class="o">&gt;&gt;</span> <span class="n">getUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">sealed</span> <span class="kd">class</span> <span class="nc">GetUserError</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">class</span> <span class="nc">NotFoundError</span> <span class="kd">extends</span> <span class="n">GetUserError</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kd">class</span> <span class="nc">NetworkError</span> <span class="kd">extends</span> <span class="n">GetUserError</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kd">final</span> <span class="kt">int</span> <span class="n">statusCode</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Result / Either pattern 把 error 從「invisible exception」升級成「型別簽章可見的回傳值」。Caller 必須處理（編譯不過 if not handled），不會漏掉 error path。</p>
<p>代價：寫法比 throw 多一些；不是所有 codebase 都採用這個 pattern。但對核心 service 介面值得。</p>
<h3 id="4-合法值是-ab-或-c">4. 「合法值是 A、B 或 C」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：String flag + doc
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [mode] 可選值：&#39;manual&#39;、&#39;auto&#39;、&#39;hybrid&#39;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="kt">String</span> <span class="n">mode</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：enum
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">enum</span> <span class="n">Mode</span> <span class="p">{</span> <span class="n">manual</span><span class="p">,</span> <span class="n">auto</span><span class="p">,</span> <span class="n">hybrid</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="kt">void</span> <span class="n">setMode</span><span class="p">(</span><span class="n">Mode</span> <span class="n">mode</span><span class="p">);</span></span></span></code></pre></div><p>String flag 是「<strong>doc 約束代替型別約束</strong>」的最常見例子。改用 enum 之後：</p>
<ul>
<li>IDE 自動補全</li>
<li>拼錯立刻編譯錯</li>
<li>新增 / 刪除 mode 時所有 caller 編譯出錯（迫使你檢查每個地方該怎麼處理）</li>
</ul>
<h3 id="5-狀態-x-才能呼叫">5. 「狀態 X 才能呼叫」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 弱：靠 doc + 執行期檢查
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 必須在 [open] 之後、[close] 之前呼叫；否則 throw [StateError]
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">write</span><span class="p">(</span><span class="kt">String</span> <span class="n">data</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 強：typestate / phantom types（Rust 友善，Dart 較吃力）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">OpenConnection</span> <span class="p">{</span> <span class="kt">void</span> <span class="n">write</span><span class="p">(</span><span class="kt">String</span> <span class="n">data</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">class</span> <span class="nc">ClosedConnection</span> <span class="p">{</span> <span class="cm">/* no write method */</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">OpenConnection</span> <span class="n">open</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">ClosedConnection</span> <span class="n">close</span><span class="p">(</span><span class="n">OpenConnection</span> <span class="n">conn</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span></span></span></code></pre></div><p>typestate 把「必須在某狀態下才能呼叫」變成「<strong>那個狀態才存在那個方法</strong>」。Rust / Haskell 寫起來最自然；Dart / Java 可以用建構子分流模擬，但 ergonomic 較差。</p>
<p>對核心 lifecycle（connection、transaction、stream subscription）值得用；一般 service 不必。</p>
<h3 id="6-兩個參數互斥某參數有時必填">6. 「兩個參數互斥」「某參數有時必填」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 弱：positional args + doc
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 同時提供 [token] 和 [credentials] 會 throw
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 至少要提供一個
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="n">User</span> <span class="n">auth</span><span class="p">(</span><span class="kt">String</span><span class="o">?</span> <span class="n">token</span><span class="p">,</span> <span class="n">Credentials</span><span class="o">?</span> <span class="n">credentials</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 強：sealed class 表達互斥
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="n">sealed</span> <span class="kd">class</span> <span class="nc">AuthMethod</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="kd">class</span> <span class="nc">TokenAuth</span> <span class="kd">extends</span> <span class="n">AuthMethod</span> <span class="p">{</span> <span class="kd">final</span> <span class="kt">String</span> <span class="n">token</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kd">class</span> <span class="nc">CredentialsAuth</span> <span class="kd">extends</span> <span class="n">AuthMethod</span> <span class="p">{</span> <span class="kd">final</span> <span class="n">Credentials</span> <span class="n">creds</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">User</span> <span class="n">auth</span><span class="p">(</span><span class="n">AuthMethod</span> <span class="n">method</span><span class="p">);</span></span></span></code></pre></div><p>「至少一個 / 至多一個 / 互斥」這類條件用 sealed class / discriminated union 表達。caller 看到型別就知道兩條路擇一，不需要 doc 說明組合規則。</p>
<h3 id="7-這個-collection-是-read-only--不要修改">7. 「這個 collection 是 read-only / 不要修改」</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 弱：靠 doc 約定
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// 不要修改回傳的 list
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 強：immutable collection 型別
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="n">List</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">List</span><span class="p">.</span><span class="n">unmodifiable</span><span class="p">(</span><span class="n">_items</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// 或：
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="n">Iterable</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="n">_items</span><span class="p">;</span>  <span class="c1">// Iterable 不暴露 mutation
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 或（用 built_collection）：
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="n">BuiltList</span><span class="o">&lt;</span><span class="n">Item</span><span class="o">&gt;</span> <span class="n">getItems</span><span class="p">();</span></span></span></code></pre></div><p>「請別修改」doc 警告靠的是「使用者願意讀且記住」，型別約束是強制的。</p>
<h3 id="8-測量單位公里-vs-英里秒-vs-毫秒">8. 「測量單位」（公里 vs 英里、秒 vs 毫秒）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 弱：靠 doc 標單位
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [timeout] 單位：毫秒
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">setTimeout</span><span class="p">(</span><span class="kt">int</span> <span class="n">timeout</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 強：用語義型別
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">setTimeout</span><span class="p">(</span><span class="n">Duration</span> <span class="n">timeout</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">setTimeout</span><span class="p">(</span><span class="n">Duration</span><span class="p">(</span><span class="nl">seconds:</span> <span class="m">30</span><span class="p">));</span>  <span class="o">//</span> <span class="err">不需要記得是哪個單位</span></span></span></code></pre></div><p>混淆單位是真實事故來源（Mars Climate Orbiter 級別的）。<code>Duration</code> / <code>Money</code> / <code>Distance</code> 等領域 wrapper 型別把單位編進型別系統，呼叫端不會傳錯。</p>
<hr>
<h2 id="型別表達不了的部分doc-仍是該寫的家">型別表達不了的部分（doc 仍是該寫的家）</h2>
<p>把可下移的下移之後，doc 還剩什麼？這些是型別表達不了的：</p>
<h3 id="1-業務動機--為什麼這個契約存在">1. 業務動機 / 為什麼這個契約存在</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 會員價只能用 wallet 付款
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// （業務規則：會員價是 wallet 餘額的折扣回饋）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">chargeMemberPrice</span><span class="p">(</span><span class="n">Member</span> <span class="n">m</span><span class="p">);</span></span></span></code></pre></div><p>「為什麼只能用 wallet」是業務規則，不在型別系統的射程內。這類<strong>有來源的業務動機</strong>仍然要寫 doc——但要有來源，不是憑想像。</p>
<h3 id="2-性能特性">2. 性能特性</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// O(log n) 查詢；插入 O(n)
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">T</span> <span class="n">find</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">);</span></span></span></code></pre></div><p>Big-O / 延遲特性 / 記憶體 footprint 等性能契約，型別表達不了。如果這個性能特性是 caller 需要知道才能正確選用（例如「這個 method 不適合在迴圈裡呼叫」），就要寫進 doc。</p>
<h3 id="3-對外部系統的副作用">3. 對外部系統的副作用</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 寫入 audit log（第三方系統，可能延遲到資料庫）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">recordTransaction</span><span class="p">(</span><span class="n">Tx</span> <span class="n">tx</span><span class="p">);</span></span></span></code></pre></div><p>跟外部系統的互動（log、analytics、cache invalidation、cloud sync）是型別表達不了的副作用。caller 需要知道這些副作用才能規劃整體流程。</p>
<h3 id="4-時序契約eventually-consistentretry-行為">4. 時序契約（eventually consistent、retry 行為）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 寫入後最多 1 秒內所有 read replica 會看到新值
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">Future</span><span class="o">&lt;</span><span class="kt">void</span><span class="o">&gt;</span> <span class="n">updateProfile</span><span class="p">(</span><span class="n">Profile</span> <span class="n">p</span><span class="p">);</span></span></span></code></pre></div><p>「最多多久內 consistent」「失敗多少次後放棄 retry」「某事件多久觸發一次」——這類<strong>跨呼叫、跨時間的契約</strong>，型別系統無法表達。</p>
<h3 id="5-使用情境的限制threading--isolation">5. 使用情境的限制（threading / isolation）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 必須在 main isolate 呼叫；否則 throw `IsolateError`
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">registerPlatformChannel</span><span class="p">(</span><span class="kt">String</span> <span class="n">name</span><span class="p">);</span></span></span></code></pre></div><p>「哪個 thread / isolate / context 才能呼叫」這類資訊，多數型別系統無法強制（Rust 的 Send/Sync 是少數例外）。</p>
<h3 id="6-跨方法-invariant">6. 跨方法 invariant</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">/// 跟 [withdraw] 配對使用：每次 [reserve] 之後必須對應一次
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">/// [withdraw] 或 [release]，否則餘額會被 reserved 卡住
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="n">reserve</span><span class="p">(</span><span class="n">Decimal</span> <span class="n">amount</span><span class="p">);</span></span></span></code></pre></div><p>「呼叫了 X 之後必須在 Y 時間內呼叫 Z」這類<strong>跨方法的 protocol</strong>，typestate 能部分表達但寫法繁瑣，多數情況靠 doc 是合理的。</p>
<hr>
<h2 id="各語言實際範例">各語言實際範例</h2>
<h3 id="dartnull-safety-的影響">Dart：null safety 的影響</h3>
<p>Dart 2.12 引入 sound null safety 後，<strong>至少消除了 30% 的 doc 內容</strong>——不再需要寫「可為 null」「不可為 null」「null 時的行為」。</p>
<p>升級前後對比：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 前（Dart 2.10）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">/// [name] 可為 null
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">/// 找不到時回傳 null
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kt">String</span> <span class="n">name</span><span class="p">;</span>  <span class="c1">// 實際可能為 null，doc 提醒
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">User</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>  <span class="c1">// 實際可能為 null
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 後（Dart 3.x）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="kd">class</span> <span class="nc">User</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="kt">String</span><span class="o">?</span> <span class="n">name</span><span class="p">;</span>  <span class="c1">// 型別說明
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">User</span><span class="o">?</span> <span class="n">findUser</span><span class="p">(</span><span class="kt">String</span> <span class="n">id</span><span class="p">);</span>  <span class="o">//</span> <span class="err">型別說明</span></span></span></code></pre></div><p>如果你的 Dart codebase 升了 null safety 但 doc 還在寫「可為 null」之類字句，說明還沒充分利用型別系統的成果。</p>
<h3 id="rustownership-與-borrow-消除一整類-doc">Rust：ownership 與 borrow 消除一整類 doc</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// C 風格：靠 doc 警告
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="sd">/// 注意：caller 必須在 buffer 釋放前完成讀取
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">/// 不要把 buffer 傳給其他 thread
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd"></span><span class="k">fn</span> <span class="nf">process</span><span class="p">(</span><span class="n">buffer</span>: <span class="o">*</span><span class="k">const</span><span class="w"> </span><span class="kt">u8</span><span class="p">,</span><span class="w"> </span><span class="n">len</span>: <span class="kt">usize</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="c1">// Rust：型別表達
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="k">fn</span> <span class="nf">process</span><span class="p">(</span><span class="n">buffer</span>: <span class="kp">&amp;</span><span class="p">[</span><span class="kt">u8</span><span class="p">]);</span><span class="w">  </span><span class="c1">// borrow，編譯期保證 lifetime
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="k">fn</span> <span class="nf">process_owned</span><span class="p">(</span><span class="n">buffer</span>: <span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">u8</span><span class="o">&gt;</span><span class="p">);</span><span class="w">  </span><span class="c1">// own，move 後 caller 不能再用
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="k">fn</span> <span class="nf">process_shared</span><span class="p">(</span><span class="n">buffer</span>: <span class="nc">Arc</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">u8</span><span class="p">]</span><span class="o">&gt;</span><span class="p">);</span><span class="w">  </span><span class="c1">// 跨 thread 安全共享
</span></span></span></code></pre></div><p>Rust 的 ownership / borrow 系統把記憶體管理 / 並發安全相關的 doc 幾乎完全變成型別。寫 Rust 的 function doc 多半短得驚人——大部分 contract 已經編進簽章。</p>
<h3 id="typescriptdiscriminated-union-取代條件-flag-doc">TypeScript：discriminated union 取代條件 flag doc</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 弱：靠 doc 解釋 flag 之間的關係
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="cm">/**
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="cm"> * @param type &#39;success&#39; or &#39;error&#39;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="cm"> * @param data 當 type=&#39;success&#39; 時必填，否則為 null
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="cm"> * @param error 當 type=&#39;error&#39; 時必填，否則為 null
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kr">interface</span> <span class="nx">Response</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kr">type</span><span class="o">:</span> <span class="kt">string</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">data?</span>: <span class="kt">any</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nx">error?</span>: <span class="kt">string</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// 強：discriminated union
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="kr">type</span> <span class="nx">Response</span> <span class="o">=</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="o">|</span> <span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="s1">&#39;success&#39;</span><span class="p">;</span> <span class="nx">data</span>: <span class="kt">ResponseData</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="o">|</span> <span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="s1">&#39;error&#39;</span><span class="p">;</span> <span class="nx">error</span>: <span class="kt">string</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1">// 使用時 TypeScript narrowing：
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="kr">type</span> <span class="o">===</span> <span class="s1">&#39;success&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>  <span class="c1">// 型別已知是 ResponseData
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">error</span><span class="p">);</span>  <span class="c1">// 型別已知是 string
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>discriminated union 把「flag 跟其他欄位的關聯」編進型別。這比 doc 警告強多了。</p>
<hr>
<h2 id="收益曲線什麼時候強型別開始邊際遞減">收益曲線：什麼時候強型別開始邊際遞減</h2>
<p>把所有可下移的 doc 都下移，是不是型別越強越好？不是。<strong>型別強化有邊際成本</strong>：</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>型別強化</th>
          <th>收益</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1. 加 null safety</td>
          <td>高</td>
          <td>消除大量 null 相關 doc + 防 NPE</td>
          <td>低（語言原生支援）</td>
      </tr>
      <tr>
          <td>2. 加 enum 取代 string flag</td>
          <td>高</td>
          <td>消除「合法值列表」doc + 編譯期檢查</td>
          <td>低</td>
      </tr>
      <tr>
          <td>3. 加 wrapper value object（PositiveInt 等）</td>
          <td>中</td>
          <td>消除範圍檢查 doc + 前移 validation</td>
          <td>中（多寫 class）</td>
      </tr>
      <tr>
          <td>4. 加 Result / Either</td>
          <td>中</td>
          <td>消除 throw doc + 強迫處理 error</td>
          <td>中（API 寫法改變、要套件 / 自寫）</td>
      </tr>
      <tr>
          <td>5. 加 typestate / phantom types</td>
          <td>低</td>
          <td>消除「狀態相關呼叫順序」doc</td>
          <td>高（程式碼變複雜、學習曲線陡）</td>
      </tr>
      <tr>
          <td>6. 加 dependent types / refinement types</td>
          <td>低</td>
          <td>編譯期完整契約</td>
          <td>極高（需要特殊語言支援）</td>
      </tr>
  </tbody>
</table>
<p>實務 sweet spot 通常落在 1-4 之間。5-6 在 systems / safety-critical 程式碼有意義，一般 app 加進去 ergonomic 變差，回收不到。</p>
<hr>
<h2 id="一個-review-的問題這條-doc-能變型別嗎">一個 review 的問題：「這條 doc 能變型別嗎？」</h2>
<p>review code 看到 doc 時，問三個問題：</p>
<ol>
<li><strong>這條 doc 描述的是輸入合法範圍嗎？</strong>
<ul>
<li>是 → 能不能用 wrapper type / refinement / enum 表達？</li>
</ul>
</li>
<li><strong>這條 doc 描述的是回傳的可能性（null、error、特殊值）嗎？</strong>
<ul>
<li>是 → 能不能用 nullable / Result / sealed class 表達？</li>
</ul>
</li>
<li><strong>這條 doc 描述的是「這時候才能呼叫」嗎？</strong>
<ul>
<li>是 → 能不能用 typestate / 不同型別的方法分流表達？</li>
</ul>
</li>
</ol>
<p>任一答案是「能」、先試型別。如果型別寫起來 ergonomic 不好（例如 wrapper class 太多、call site 變難讀）、再退回 doc——「先試型別」比「預設寫 doc」更能逼出可下移的部分。</p>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>doc 是「<strong>型別表達不了的剩餘資訊</strong>」的家——型別越強、剩餘越少。</p></blockquote>
<p>寫 doc 之前先問「能用型別表達嗎」。能 → 改型別。不能 → 寫 doc，但只寫那條型別表達不了的部分（業務動機、性能、副作用、時序契約、跨方法 protocol）。</p>
<hr>
<h2 id="收束型別系統升級是文件設計升級的契機">收束：型別系統升級是文件設計升級的契機</h2>
<p>每一次語言升級（Dart 2 → 3、TypeScript 加新型別功能、Rust 穩定新 lifetime feature），都是<strong>重新檢視既有 doc</strong> 的機會：</p>
<ul>
<li>哪些 doc 可以下移到新引入的型別功能？</li>
<li>下移之後，剩下的 doc 是不是更精準了？</li>
<li>是不是有新的型別組合能表達以前只能靠 doc 的契約？</li>
</ul>
<p>把語言升級當成 doc 整理的契機，不只是「換個編譯器」。<strong>程式碼品質的關鍵改善往往來自把約定升級為約束</strong>——doc 是約定，型別是約束。約定靠人記住，約束靠工具強制。每次升級都是一次「把約定變約束」的機會窗口。</p>
<p>寫到「三行 doc 解釋一個 function 的合法輸入範圍」這個訊號時、自問：<strong>「這三行能不能變成型別簽章？」</strong>——多半可以。</p>
]]></content:encoded></item><item><title>設計瑕疵還是避免過度設計？YAGNI 的真實適用條件</title><link>https://tarrragon.github.io/blog/record/%E8%A8%AD%E8%A8%88%E7%91%95%E7%96%B5%E9%82%84%E6%98%AF%E9%81%BF%E5%85%8D%E9%81%8E%E5%BA%A6%E8%A8%AD%E8%A8%88yagni-%E7%9A%84%E7%9C%9F%E5%AF%A6%E9%81%A9%E7%94%A8%E6%A2%9D%E4%BB%B6/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E8%A8%AD%E8%A8%88%E7%91%95%E7%96%B5%E9%82%84%E6%98%AF%E9%81%BF%E5%85%8D%E9%81%8E%E5%BA%A6%E8%A8%AD%E8%A8%88yagni-%E7%9A%84%E7%9C%9F%E5%AF%A6%E9%81%A9%E7%94%A8%E6%A2%9D%E4%BB%B6/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：YAGNI 不是「永遠選最受限選項」的原則，是「不為未來投入額外成本」的原則。
&lt;strong>判斷工具&lt;/strong>：成本對稱性、可逆性、領域先驗——三軸框架。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點一個常見的工程爭論">起點：一個常見的工程爭論&lt;/h2>
&lt;p>「最早的設計者沒考慮到多個監聽需求，這算設計瑕疵，還是避免過度設計？」&lt;/p>
&lt;p>這類問題在 code review、事故檢討、技術選型討論裡反覆出現。指控太重會打擊個別工程師的判斷力信心，放任又會讓同類事故反覆發生。&lt;/p>
&lt;p>要釐清這個爭論，得先回到 YAGNI 原則的真實定義——很多被當成 YAGNI 的例子根本不在它的射程內。&lt;/p>
&lt;hr>
&lt;h2 id="yagni-的真實範圍">YAGNI 的真實範圍&lt;/h2>
&lt;p>YAGNI（You Aren&amp;rsquo;t Gonna Need It）的原意是：&lt;strong>不要投入額外成本去蓋你尚未需要的東西&lt;/strong>。它防的是這類情境：&lt;/p>
&lt;ul>
&lt;li>「我先寫個 plugin 系統，未來可以擴充」（成本：協議設計、抽象層、擴充點測試）&lt;/li>
&lt;li>「我先做多語系，未來會國際化」（成本：i18n 框架、所有字串外移）&lt;/li>
&lt;li>「我先支援多資料庫」（成本：repository 抽象、SQL 方言處理）&lt;/li>
&lt;li>「我先建多租戶切割」（成本：資料 schema 加 tenant 欄位、所有 query 加過濾）&lt;/li>
&lt;/ul>
&lt;p>這些選擇的共通特徵是：&lt;strong>為了未來付出當下的具體成本&lt;/strong>——抽象層、額外測試、複雜配置、學習負擔。YAGNI 說：別付，等真正需要再付，因為很可能你永遠不需要。&lt;/p>
&lt;p>但很多被指控為「過度設計」的選擇其實&lt;strong>沒有 upfront cost 差異&lt;/strong>。例如：&lt;/p>
&lt;ul>
&lt;li>Stream 工具用單訂閱版本還是廣播版本：建構子多打 11 個字元&lt;/li>
&lt;li>&lt;code>var&lt;/code> 還是 &lt;code>final&lt;/code>：3 個字元&lt;/li>
&lt;li>ID 用 &lt;code>int&lt;/code> 還是 &lt;code>String&lt;/code>（UUID）：抽象層成本一樣&lt;/li>
&lt;li>API 設計成同步還是 async：簽章只差 &lt;code>Future&amp;lt;&amp;gt;&lt;/code> 包裝&lt;/li>
&lt;li>Class 預設可繼承還是 sealed：一個 modifier&lt;/li>
&lt;li>Database column 預設 nullable 還是 NOT NULL：一個 keyword&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>這些不在 YAGNI 的射程內&lt;/strong>。把它們當成 YAGNI 來防禦會選錯方向。&lt;/p>
&lt;hr>
&lt;h2 id="真正的判斷軸成本不對稱性">真正的判斷軸：成本不對稱性&lt;/h2>
&lt;p>判斷「該不該選更通用的選項」，跑三個軸。&lt;/p>
&lt;h3 id="軸-1成本對稱性">軸 1：成本對稱性&lt;/h3>
&lt;p>「選擇 A 比選擇 B 多付出多少當下成本？」&lt;/p>
&lt;ul>
&lt;li>&lt;strong>對稱&lt;/strong>（成本相當、差幾個字元、無新概念）：選&lt;strong>未來更可能需要&lt;/strong>的那個——這不是過度設計，是合理 default&lt;/li>
&lt;li>&lt;strong>不對稱&lt;/strong>（一邊明顯較貴、要多寫框架、多加抽象、多學概念）：YAGNI 適用，選便宜的，需要時再升級&lt;/li>
&lt;/ul>
&lt;h3 id="軸-2改變決定的成本">軸 2：改變決定的成本&lt;/h3>
&lt;p>「如果選錯了，未來修正要付出什麼？」&lt;/p>
&lt;ul>
&lt;li>&lt;strong>可逆&lt;/strong>（一行改完、無 API 契約變動、無資料遷移）：YAGNI 適用，先選簡單的&lt;/li>
&lt;li>&lt;strong>不可逆 / 修正昂貴&lt;/strong>（牽動 API 契約、資料庫 schema、客戶端版本相容性、第三方 integration）：偏向預先選擇通用的&lt;/li>
&lt;/ul>
&lt;h3 id="軸-3領域先驗domain-prior">軸 3：領域先驗（domain prior）&lt;/h3>
&lt;p>「這個領域裡、這個模式發生的機率有多高？」——「先驗」（prior）借自 Bayesian 統計、用來指「在沒看到具體證據前、我們對某事發生機率的合理預期」。在工程領域、這個機率來自累積的領域知識（多視角同步、retry、併發、認證⋯⋯這些 pattern 的歷史發生率）。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>強先驗&lt;/strong>（教科書級別）：多視角狀態同步是廣播、有用戶系統一定有 logged-in / anonymous 兩種、長時間運行服務一定會有 retry 需求、有交易就會有併發&lt;/li>
&lt;li>&lt;strong>弱先驗&lt;/strong>（純臆測）：「未來可能會有 plugin 機制吧」「未來可能要換資料庫吧」「未來可能要支援其他平台吧」&lt;/li>
&lt;/ul>
&lt;h3 id="三軸的綜合判斷">三軸的綜合判斷&lt;/h3>
&lt;p>任一軸顯著偏向「該選通用」，YAGNI 就不適用。&lt;/p>
&lt;p>&lt;strong>選通用不是過度設計，是對工具屬性與領域常識的尊重&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="案例對照兩個極端">案例對照：兩個極端&lt;/h2>
&lt;h3 id="案例-astream-預設選錯">案例 A：Stream 預設選錯&lt;/h3>
&lt;p>某個事件廣播 service 用了 &lt;code>StreamController()&lt;/code> 預設建構子（單訂閱）。當下只有一個訂閱者，運作正常數個月。後來加第二個訂閱者，瞬間 throw &lt;code>Bad state: Stream has already been listened to&lt;/code>。&lt;/p>
&lt;p>跑三軸：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>成本對稱性&lt;/strong>：對稱（差 11 個字元、零認知負擔）&lt;/li>
&lt;li>&lt;strong>可逆性&lt;/strong>：中等偏高（事故必須在 production 暴露才會發現，要審所有訂閱方、改實作 + mock）&lt;/li>
&lt;li>&lt;strong>領域先驗&lt;/strong>：強（pub-sub / 事件廣播場景天生多訂閱）&lt;/li>
&lt;/ul>
&lt;p>三軸都指向廣播版本。&lt;strong>這是設計瑕疵&lt;/strong>——不是因為「沒考慮多訂閱」，而是&lt;strong>在三軸都不利於單訂閱的情況下選了單訂閱&lt;/strong>。&lt;/p>
&lt;blockquote>
&lt;p>完整事故重現、單訂閱 vs broadcast 的程式碼對比、修復決策過程：&lt;a href="https://tarrragon.github.io/blog/work-log/dart-streamcontrollersingle-subscription-vs-broadcast-%E7%9A%84%E8%A8%AD%E8%A8%88%E9%81%B8%E5%9E%8B%E5%95%8F%E9%A1%8C/" data-link-title="Dart StreamController：single-subscription vs broadcast 的設計選型問題" data-link-desc="Dart `Bad state: Stream has already been listened to.` 的根因：預設單訂閱在第二個訂閱者出現時才爆。StreamController vs .broadcast() 修復決策、與 Rx / .obs 的比較。">Dart StreamController：single-subscription vs broadcast 的事故實錄&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h3 id="案例-b建立-plugin-系統">案例 B：建立 plugin 系統&lt;/h3>
&lt;p>「我先建個 plugin 系統，未來功能模組可以動態擴充」——典型的 over-engineering 焦慮表現。&lt;/p>
&lt;p>跑三軸：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>成本對稱性&lt;/strong>：嚴重不對稱（plugin 系統需要設計協議、加載機制、版本管理、隔離測試）&lt;/li>
&lt;li>&lt;strong>可逆性&lt;/strong>：可逆（之後要做的話成本跟現在做差不多）&lt;/li>
&lt;li>&lt;strong>領域先驗&lt;/strong>：弱（多數應用程式不會有第三方擴充需求）&lt;/li>
&lt;/ul>
&lt;p>三軸都指向「先別做」。&lt;strong>這是 YAGNI 的標準適用情境&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：YAGNI 不是「永遠選最受限選項」的原則，是「不為未來投入額外成本」的原則。
<strong>判斷工具</strong>：成本對稱性、可逆性、領域先驗——三軸框架。</p></blockquote>
<hr>
<h2 id="起點一個常見的工程爭論">起點：一個常見的工程爭論</h2>
<p>「最早的設計者沒考慮到多個監聽需求，這算設計瑕疵，還是避免過度設計？」</p>
<p>這類問題在 code review、事故檢討、技術選型討論裡反覆出現。指控太重會打擊個別工程師的判斷力信心，放任又會讓同類事故反覆發生。</p>
<p>要釐清這個爭論，得先回到 YAGNI 原則的真實定義——很多被當成 YAGNI 的例子根本不在它的射程內。</p>
<hr>
<h2 id="yagni-的真實範圍">YAGNI 的真實範圍</h2>
<p>YAGNI（You Aren&rsquo;t Gonna Need It）的原意是：<strong>不要投入額外成本去蓋你尚未需要的東西</strong>。它防的是這類情境：</p>
<ul>
<li>「我先寫個 plugin 系統，未來可以擴充」（成本：協議設計、抽象層、擴充點測試）</li>
<li>「我先做多語系，未來會國際化」（成本：i18n 框架、所有字串外移）</li>
<li>「我先支援多資料庫」（成本：repository 抽象、SQL 方言處理）</li>
<li>「我先建多租戶切割」（成本：資料 schema 加 tenant 欄位、所有 query 加過濾）</li>
</ul>
<p>這些選擇的共通特徵是：<strong>為了未來付出當下的具體成本</strong>——抽象層、額外測試、複雜配置、學習負擔。YAGNI 說：別付，等真正需要再付，因為很可能你永遠不需要。</p>
<p>但很多被指控為「過度設計」的選擇其實<strong>沒有 upfront cost 差異</strong>。例如：</p>
<ul>
<li>Stream 工具用單訂閱版本還是廣播版本：建構子多打 11 個字元</li>
<li><code>var</code> 還是 <code>final</code>：3 個字元</li>
<li>ID 用 <code>int</code> 還是 <code>String</code>（UUID）：抽象層成本一樣</li>
<li>API 設計成同步還是 async：簽章只差 <code>Future&lt;&gt;</code> 包裝</li>
<li>Class 預設可繼承還是 sealed：一個 modifier</li>
<li>Database column 預設 nullable 還是 NOT NULL：一個 keyword</li>
</ul>
<p><strong>這些不在 YAGNI 的射程內</strong>。把它們當成 YAGNI 來防禦會選錯方向。</p>
<hr>
<h2 id="真正的判斷軸成本不對稱性">真正的判斷軸：成本不對稱性</h2>
<p>判斷「該不該選更通用的選項」，跑三個軸。</p>
<h3 id="軸-1成本對稱性">軸 1：成本對稱性</h3>
<p>「選擇 A 比選擇 B 多付出多少當下成本？」</p>
<ul>
<li><strong>對稱</strong>（成本相當、差幾個字元、無新概念）：選<strong>未來更可能需要</strong>的那個——這不是過度設計，是合理 default</li>
<li><strong>不對稱</strong>（一邊明顯較貴、要多寫框架、多加抽象、多學概念）：YAGNI 適用，選便宜的，需要時再升級</li>
</ul>
<h3 id="軸-2改變決定的成本">軸 2：改變決定的成本</h3>
<p>「如果選錯了，未來修正要付出什麼？」</p>
<ul>
<li><strong>可逆</strong>（一行改完、無 API 契約變動、無資料遷移）：YAGNI 適用，先選簡單的</li>
<li><strong>不可逆 / 修正昂貴</strong>（牽動 API 契約、資料庫 schema、客戶端版本相容性、第三方 integration）：偏向預先選擇通用的</li>
</ul>
<h3 id="軸-3領域先驗domain-prior">軸 3：領域先驗（domain prior）</h3>
<p>「這個領域裡、這個模式發生的機率有多高？」——「先驗」（prior）借自 Bayesian 統計、用來指「在沒看到具體證據前、我們對某事發生機率的合理預期」。在工程領域、這個機率來自累積的領域知識（多視角同步、retry、併發、認證⋯⋯這些 pattern 的歷史發生率）。</p>
<ul>
<li><strong>強先驗</strong>（教科書級別）：多視角狀態同步是廣播、有用戶系統一定有 logged-in / anonymous 兩種、長時間運行服務一定會有 retry 需求、有交易就會有併發</li>
<li><strong>弱先驗</strong>（純臆測）：「未來可能會有 plugin 機制吧」「未來可能要換資料庫吧」「未來可能要支援其他平台吧」</li>
</ul>
<h3 id="三軸的綜合判斷">三軸的綜合判斷</h3>
<p>任一軸顯著偏向「該選通用」，YAGNI 就不適用。</p>
<p><strong>選通用不是過度設計，是對工具屬性與領域常識的尊重</strong>。</p>
<hr>
<h2 id="案例對照兩個極端">案例對照：兩個極端</h2>
<h3 id="案例-astream-預設選錯">案例 A：Stream 預設選錯</h3>
<p>某個事件廣播 service 用了 <code>StreamController()</code> 預設建構子（單訂閱）。當下只有一個訂閱者，運作正常數個月。後來加第二個訂閱者，瞬間 throw <code>Bad state: Stream has already been listened to</code>。</p>
<p>跑三軸：</p>
<ul>
<li><strong>成本對稱性</strong>：對稱（差 11 個字元、零認知負擔）</li>
<li><strong>可逆性</strong>：中等偏高（事故必須在 production 暴露才會發現，要審所有訂閱方、改實作 + mock）</li>
<li><strong>領域先驗</strong>：強（pub-sub / 事件廣播場景天生多訂閱）</li>
</ul>
<p>三軸都指向廣播版本。<strong>這是設計瑕疵</strong>——不是因為「沒考慮多訂閱」，而是<strong>在三軸都不利於單訂閱的情況下選了單訂閱</strong>。</p>
<blockquote>
<p>完整事故重現、單訂閱 vs broadcast 的程式碼對比、修復決策過程：<a href="/blog/work-log/dart-streamcontrollersingle-subscription-vs-broadcast-%E7%9A%84%E8%A8%AD%E8%A8%88%E9%81%B8%E5%9E%8B%E5%95%8F%E9%A1%8C/" data-link-title="Dart StreamController：single-subscription vs broadcast 的設計選型問題" data-link-desc="Dart `Bad state: Stream has already been listened to.` 的根因：預設單訂閱在第二個訂閱者出現時才爆。StreamController vs .broadcast() 修復決策、與 Rx / .obs 的比較。">Dart StreamController：single-subscription vs broadcast 的事故實錄</a>。</p></blockquote>
<h3 id="案例-b建立-plugin-系統">案例 B：建立 plugin 系統</h3>
<p>「我先建個 plugin 系統，未來功能模組可以動態擴充」——典型的 over-engineering 焦慮表現。</p>
<p>跑三軸：</p>
<ul>
<li><strong>成本對稱性</strong>：嚴重不對稱（plugin 系統需要設計協議、加載機制、版本管理、隔離測試）</li>
<li><strong>可逆性</strong>：可逆（之後要做的話成本跟現在做差不多）</li>
<li><strong>領域先驗</strong>：弱（多數應用程式不會有第三方擴充需求）</li>
</ul>
<p>三軸都指向「先別做」。<strong>這是 YAGNI 的標準適用情境</strong>。</p>
<h3 id="兩個案例的對比">兩個案例的對比</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>成本對稱性</th>
          <th>可逆性</th>
          <th>領域先驗</th>
          <th>該怎麼選</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stream 預設</td>
          <td>對稱</td>
          <td>中等偏高</td>
          <td>強</td>
          <td>提前選通用</td>
      </tr>
      <tr>
          <td>Plugin 系統</td>
          <td>嚴重不對稱</td>
          <td>可逆</td>
          <td>弱</td>
          <td>YAGNI（先別做）</td>
      </tr>
  </tbody>
</table>
<p>兩者表面看都是「未來可能需要」，但三軸框架告訴你它們是<strong>完全不同類別</strong>的決定。一概而論「該/不該為未來準備」會兩邊都做錯。</p>
<hr>
<h2 id="為什麼這類瑕疵可被原諒">為什麼這類瑕疵「可被原諒」</h2>
<p>要老實講：<strong>指出某個選擇是設計瑕疵，不等於把責任全部推給個別工程師</strong>。</p>
<p>同類型瑕疵在實務上極常見，原因往往是系統性陷阱。</p>
<h3 id="1-語言--工具的預設值誤導">1. 語言 / 工具的預設值誤導</h3>
<p>很多語言把「需要明確選擇」的東西做成「最少打字的預設」：</p>
<ul>
<li>Dart 的 <code>StreamController()</code> 是 single-subscription</li>
<li>多數 SQL 的 column 預設 nullable</li>
<li>JavaScript 的 <code>==</code> 預設寬鬆比對</li>
<li>多數語言的 class 預設可繼承</li>
<li>HTTP 預設不加密</li>
<li>多數語言的 mutable 是 default</li>
</ul>
<p>這些預設都把多數人推向「比較容易出錯但不立即爆」的選項。<strong>API 設計把成本均衡的選擇做成「便宜便輸出受限」vs「貴一點輸出通用」是 framework 設計的責任轉嫁</strong>——把跨用例的判斷成本丟給用戶。</p>
<h3 id="2-領域知識需要被觸發過才會內化">2. 領域知識需要被觸發過才會內化</h3>
<p>很多事是遇過一次才會記得。「stream 預設是單訂閱」「nullable column 之後加 NOT NULL 要 backfill」「同步 API 之後改 async 是 breaking change」——這些不是經驗少的問題，是這些事實<strong>需要遇到才會內化進直覺判斷</strong>。</p>
<p>新人讀文件不會看到、code review 不會自動 catch、靜態分析不會主動警告——只能等某次遇到。</p>
<h3 id="3-失敗模式的低調性掩蓋風險">3. 失敗模式的低調性掩蓋風險</h3>
<p>很多設計瑕疵的失敗模式只在特定觸發條件下顯現：</p>
<ul>
<li>Stream 多訂閱限制只在第二次 <code>listen()</code> 時暴露</li>
<li>Mutable shared state 的 race condition 只在高併發下爆</li>
<li>Cache 失效邏輯只在 cache miss 模式變化時出問題</li>
<li>API 沒做 idempotent 只在重試時出現重複</li>
</ul>
<p>平常測試跑都過，給人「沒問題」的錯覺。<strong>沒有立即反饋的設計瑕疵 = 隱形的技術債</strong>。</p>
<h3 id="4-工具替代品掩蓋知識需求">4. 工具替代品掩蓋知識需求</h3>
<p>有些底層概念被高層框架封裝後，使用者根本不會碰到，所以「應該知道」的知識沒有被反覆強化。例如：</p>
<ul>
<li>Flutter 開發者多用 GetX / Riverpod / Bloc，極少碰 raw <code>StreamController</code></li>
<li>ORM 用戶多不寫 SQL，極少思考 query plan</li>
<li>雲端 SDK 用戶多不思考 retry / backoff，極少接觸底層 HTTP</li>
</ul>
<p>當有一天必須繞過框架直接用底層工具時，那個事故就會發生。</p>
<h3 id="結論">結論</h3>
<p>設計者只承擔最後一棒。要把同類瑕疵變少，<strong>修補方向在制度層面</strong>。</p>
<hr>
<h2 id="制度層面的補強">制度層面的補強</h2>
<p>要把「該選通用 default 但選了受限預設」的錯誤變少，個人記憶不可靠，要靠三層機制。</p>
<h3 id="機制-1介面層的-review-checklist">機制 1：介面層的 review checklist</h3>
<p>把容易出錯的 default 列入 PR review 檢查清單。例如：</p>
<ul>
<li>Service 對外暴露 <code>Stream&lt;T&gt;</code> 時、預設用 broadcast；用 single 要在註解寫明理由</li>
<li>資料庫 column 預設用 NOT NULL；nullable 要在註解寫明業務理由</li>
<li>公開 API 預設用 async；sync 要寫明理由</li>
<li>公開類別預設用 sealed / final；可繼承要寫明理由</li>
<li>HTTP 預設用 HTTPS；plain HTTP 要寫明理由</li>
</ul>
<p>把「需要記得」變成「review 強制檢查」。Checklist 不需要多，每個項目對應一個遇過的事故。</p>
<h3 id="機制-2架構規範把選擇從-default-取消">機制 2：架構規範把選擇從 default 取消</h3>
<p>更徹底的做法是用工具或規範<strong>禁掉問題 default</strong>：</p>
<ul>
<li>App 層 service 禁用 raw <code>StreamController</code>，強制用框架的廣播原語</li>
<li>用 lint rule 警告 <code>StreamController()</code> 的無參數呼叫</li>
<li>DB schema migration 工具預設產出 NOT NULL，nullable 要明確指定</li>
<li>API gateway 預設 deny，要顯式 allow 才放行</li>
</ul>
<p>這把選擇從「需要記得」變成「<strong>不需要選，做錯會被擋</strong>」。是最高效的補強。</p>
<h3 id="機制-3領域先驗清單">機制 3：領域先驗清單</h3>
<p>每個團隊應該維護一份「<strong>我們的領域裡這些事一定會發生</strong>」的清單。範例：</p>
<p>POS 系統：</p>
<ul>
<li>一台主機要服務多視角（多顯示螢幕、多通知模組）</li>
<li>會員身份會即時切換</li>
<li>有離線運作需求</li>
<li>多分店不同設定</li>
</ul>
<p>電商：</p>
<ul>
<li>商品價格會變動，歷史訂單要保留下單當時的價格</li>
<li>庫存會超賣，需要 reserve / commit 機制</li>
<li>退款是必然發生的，不是 edge case</li>
<li>客戶會有多個收件地址</li>
</ul>
<p>新功能設計時對照清單——強領域先驗就直接設計進去，<strong>不必每次重新評估</strong>。新進團隊成員也能快速吸收領域常識。</p>
<hr>
<h2 id="一個能套到無數情境的-heuristic">一個能套到無數情境的 heuristic</h2>
<p>把整個討論濃縮成一句話：</p>
<blockquote>
<p>當你的選擇「<strong>沒有 upfront cost 差異</strong>」時、就該選未來自由度高的那個。</p></blockquote>
<p>這個 heuristic 能套到無數技術決定：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>「便宜但受限」</th>
          <th>「同樣便宜但通用」</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Stream 廣播</td>
          <td><code>StreamController()</code></td>
          <td><code>StreamController.broadcast()</code></td>
      </tr>
      <tr>
          <td>集合不可變性</td>
          <td><code>var list = [1, 2]</code></td>
          <td><code>final list = const [1, 2]</code></td>
      </tr>
      <tr>
          <td>API 回傳值</td>
          <td>同步 method</td>
          <td><code>Future&lt;&gt;</code> 包裝</td>
      </tr>
      <tr>
          <td>函式參數</td>
          <td>positional args</td>
          <td>named args</td>
      </tr>
      <tr>
          <td>Class 設計</td>
          <td>預設可繼承</td>
          <td><code>sealed</code> / <code>final class</code></td>
      </tr>
      <tr>
          <td>Resource handle</td>
          <td>manual cleanup</td>
          <td>RAII / <code>using</code> block</td>
      </tr>
      <tr>
          <td>Time</td>
          <td>local time</td>
          <td>UTC + timezone metadata</td>
      </tr>
      <tr>
          <td>ID 型別</td>
          <td><code>int</code> auto-increment</td>
          <td><code>String</code> (UUID)</td>
      </tr>
      <tr>
          <td>Money</td>
          <td><code>double</code></td>
          <td>專用 <code>Decimal</code> 型別</td>
      </tr>
      <tr>
          <td>字串編碼</td>
          <td>平台預設</td>
          <td>顯式 UTF-8</td>
      </tr>
  </tbody>
</table>
<p>這些都不是「過度設計」，是<strong>在零成本差異下選擇未來自由度更高的選項</strong>。YAGNI 不適用——YAGNI 的成本門檻在這裡根本不存在。</p>
<hr>
<h2 id="反向校正什麼時候該堅持-yagni">反向校正：什麼時候該堅持 YAGNI？</h2>
<p>為了避免本文被讀成「永遠選通用」，補一個反向案例。</p>
<p>YAGNI 在這些情境是對的：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 YAGNI 適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「先做個 admin 後台，未來方便」</td>
          <td>成本巨大，需求未確認，可逆</td>
      </tr>
      <tr>
          <td>「先支援自訂主題系統」</td>
          <td>成本中等，弱領域先驗，可逆</td>
      </tr>
      <tr>
          <td>「先做 API rate limiting」</td>
          <td>成本中等，現階段流量沒問題，可逆</td>
      </tr>
      <tr>
          <td>「先設計 multi-region 部署」</td>
          <td>成本巨大，多數產品永遠單 region</td>
      </tr>
      <tr>
          <td>「先抽 service 層」</td>
          <td>成本中等，function 直接呼叫已經夠用</td>
      </tr>
  </tbody>
</table>
<p>這些都是<strong>為了未來付出當下具體成本</strong>——抽象層、新概念、額外測試、配置複雜度。YAGNI 在這些情境會帶你做出對的選擇。</p>
<p>判斷的差異是：<strong>這個決定是「選哪個免費選項」，還是「要不要付一筆額外開發成本」？</strong> 前者三軸框架；後者 YAGNI。</p>
<hr>
<h2 id="總結">總結</h2>
<p>YAGNI vs 過度設計的爭論，常常因為兩邊在用不同定義而無法收斂。釐清如下：</p>
<blockquote>
<p><strong>YAGNI 適用於「為了未來而付出當下的具體成本」</strong>
<strong>不適用於「在成本相當的選項中選擇更通用的那個」</strong></p></blockquote>
<p>判斷時跑三軸：</p>
<ol>
<li><strong>成本對稱性</strong>：兩個選項的 upfront cost 是否相當？</li>
<li><strong>可逆性</strong>：選錯的話修正昂貴嗎？</li>
<li><strong>領域先驗</strong>：這個模式在領域裡發生機率多高？</li>
</ol>
<p>任一軸顯著偏向「該選通用」，YAGNI 就不適用，這不是過度設計。</p>
<p>回到開頭問題——「最早的設計者沒考慮到多個監聽需求、這算設計瑕疵還是避免過度設計？」答案<strong>取決於這三軸的具體狀況</strong>、不能一概而論。</p>
<p>但如果像 Stream 這個案例、三軸全部不利於受限預設、那就是設計瑕疵。<strong>只是這類瑕疵反映的是工具預設與領域知識內化的系統性問題、不是個別工程師的判斷力不足</strong>——修補方向是制度而非個人責備。</p>
<h3 id="一句話帶走">一句話帶走</h3>
<p>日常情境中、把三軸壓縮成一個問題就夠用：</p>
<blockquote>
<p>「<strong>我在多付什麼成本？</strong>」</p></blockquote>
<ul>
<li>多付<strong>抽象層、新概念、額外測試</strong> → YAGNI 適用、先別付</li>
<li>多付<strong>幾個字元、一個關鍵字</strong> → 不是 YAGNI、選通用的</li>
</ul>
<p>需要更精細的時候、再回頭跑完整三軸框架。</p>
]]></content:encoded></item><item><title>測試命名作為文件：可執行的規格說明</title><link>https://tarrragon.github.io/blog/record/%E6%B8%AC%E8%A9%A6%E5%91%BD%E5%90%8D%E4%BD%9C%E7%82%BA%E6%96%87%E4%BB%B6%E5%8F%AF%E5%9F%B7%E8%A1%8C%E7%9A%84%E8%A6%8F%E6%A0%BC%E8%AA%AA%E6%98%8E/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E6%B8%AC%E8%A9%A6%E5%91%BD%E5%90%8D%E4%BD%9C%E7%82%BA%E6%96%87%E4%BB%B6%E5%8F%AF%E5%9F%B7%E8%A1%8C%E7%9A%84%E8%A6%8F%E6%A0%BC%E8%AA%AA%E6%98%8E/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>核心命題&lt;/strong>：測試是少數&lt;strong>會自我驗證&lt;/strong>的文件——名稱說的事如果跟實際行為不符，CI 會炸。
&lt;strong>設計原則&lt;/strong>：測試命名應該讓「跳到測試檔讀名字」就能取代讀 doc。&lt;/p>&lt;/blockquote>
&lt;blockquote>
&lt;p>本篇是 &lt;a href="../function-doc-layered-design/">函式文件分層設計&lt;/a> 的 Layer 4（範例與測試）展開——把「測試命名作為可執行 spec」這個職責拉成獨立主題討論。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="起點被-ci-強制同步的-doc">起點：被 CI 強制同步的 doc&lt;/h2>
&lt;p>source code 的 doc comment 有個結構性缺陷：&lt;strong>寫得再好，code 改了 doc 沒改，doc 就在說謊&lt;/strong>。沒有任何工具強制 doc 跟 code 同步。&lt;/p>
&lt;p>測試是少數例外。一個命名為 &lt;code>removes_item_when_quantity_reaches_zero&lt;/code> 的測試，如果實際上 quantity 到 0 時沒移除，&lt;strong>測試會失敗、CI 會擋下 commit&lt;/strong>。測試名稱跟實際行為的一致性是被 CI 強制的——這讓測試成為&lt;strong>會自我驗證的文件&lt;/strong>。&lt;/p>
&lt;p>當你把這個性質有意識地利用起來，測試就不只是 regression 工具，而是&lt;strong>可執行的 API 規格&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="測試命名的三種主要模式">測試命名的三種主要模式&lt;/h2>
&lt;p>被測單元的契約大致分三類：「&lt;strong>在某狀態下回傳什麼&lt;/strong>」「&lt;strong>某操作會做什麼&lt;/strong>」「&lt;strong>何時 throw / 失敗&lt;/strong>」——對應到測試命名也分三類 pattern。每類 pattern 的命名格式不同、負責驗證契約的不同切面。&lt;/p>
&lt;h3 id="模式-1state-based狀態描述">模式 1：state-based（狀態描述）&lt;/h3>
&lt;p>「在某個狀態下，呼叫 X 會回傳 / 變成什麼」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_null_when_user_not_found&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_empty_list_when_no_items_match&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_cached_value_on_second_call&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>適合：query / read-only 操作。&lt;/p>
&lt;h3 id="模式-2scenario-based情境描述">模式 2：scenario-based（情境描述）&lt;/h3>
&lt;p>「當某條件成立時，操作會做什麼」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removes_item_when_quantity_reaches_zero&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;decreases_quantity_when_item_exists_with_quantity_above_one&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;updates_lastChangedItem_on_addItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;does_not_update_lastChangedItem_on_removeItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>適合：command / mutation 操作。注意 &lt;code>does_not_X&lt;/code> 形式——&lt;strong>negative assertion 也該寫進名字&lt;/strong>，這正是契約的一部分。&lt;/p>
&lt;h3 id="模式-3failure-mode失敗模式描述">模式 3：failure-mode（失敗模式描述）&lt;/h3>
&lt;p>「在某輸入 / 狀態下，會 throw / error / 失敗」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;throws_NotFoundException_when_id_does_not_exist&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;throws_StateError_when_called_after_dispose&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;returns_error_when_network_unavailable&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>適合：error path、edge case。&lt;strong>失敗模式是 doc 最容易漏寫的部分&lt;/strong>，但對 caller 最關鍵。&lt;/p>
&lt;hr>
&lt;h2 id="group-結構作為命名空間">Group 結構作為命名空間&lt;/h2>
&lt;p>巢狀 group 提供了「主題 → 操作 → 情境」的階層命名空間，比扁平命名更易讀：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;CartService&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;addItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;appends_when_item_not_in_cart&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;increments_quantity_when_same_item_exists&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;updates_lastChangedItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removeItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removes_when_item_exists&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;does_nothing_when_item_not_found&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;does_not_update_lastChangedItem&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="n">group&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;decreaseQuantity&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;decreases_when_quantity_above_one&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;removes_item_when_quantity_reaches_zero&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>讀者掃過 group 結構，立刻知道 &lt;code>CartService&lt;/code> 對外提供哪些操作、每個操作有哪些行為承諾——&lt;strong>這是這個 service 的 readable spec&lt;/strong>。&lt;/p>
&lt;p>工具支援：好的 IDE / test runner 會把 group 結構顯示為樹狀，跑測試時的輸出也帶階層。把這個視覺結構利用好，測試 console 本身就是 doc 瀏覽器。&lt;/p>
&lt;hr>
&lt;h2 id="把-tests-當-readable-spec-的閱讀流程">把 tests 當 readable spec 的閱讀流程&lt;/h2>
&lt;p>當你不確定一個 function 的行為時，閱讀順序通常是：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>看簽章&lt;/strong> → 知道 what / takes / returns&lt;/li>
&lt;li>&lt;strong>讀 doc&lt;/strong> → 知道契約、edge case&lt;/li>
&lt;li>&lt;strong>看實作&lt;/strong> → 知道 how&lt;/li>
&lt;li>&lt;strong>找測試&lt;/strong> → 看具體 case&lt;/li>
&lt;/ol>
&lt;p>但如果測試命名做得好，&lt;strong>順序可以對調&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>看簽章&lt;/li>
&lt;li>&lt;strong>跳到對應 test file，掃 group + test names&lt;/strong> → 看 API 支援哪些 case、各 case 的承諾&lt;/li>
&lt;li>不夠才回去讀 doc / 實作&lt;/li>
&lt;/ol>
&lt;p>這個順序的優勢：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>核心命題</strong>：測試是少數<strong>會自我驗證</strong>的文件——名稱說的事如果跟實際行為不符，CI 會炸。
<strong>設計原則</strong>：測試命名應該讓「跳到測試檔讀名字」就能取代讀 doc。</p></blockquote>
<blockquote>
<p>本篇是 <a href="../function-doc-layered-design/">函式文件分層設計</a> 的 Layer 4（範例與測試）展開——把「測試命名作為可執行 spec」這個職責拉成獨立主題討論。</p></blockquote>
<hr>
<h2 id="起點被-ci-強制同步的-doc">起點：被 CI 強制同步的 doc</h2>
<p>source code 的 doc comment 有個結構性缺陷：<strong>寫得再好，code 改了 doc 沒改，doc 就在說謊</strong>。沒有任何工具強制 doc 跟 code 同步。</p>
<p>測試是少數例外。一個命名為 <code>removes_item_when_quantity_reaches_zero</code> 的測試，如果實際上 quantity 到 0 時沒移除，<strong>測試會失敗、CI 會擋下 commit</strong>。測試名稱跟實際行為的一致性是被 CI 強制的——這讓測試成為<strong>會自我驗證的文件</strong>。</p>
<p>當你把這個性質有意識地利用起來，測試就不只是 regression 工具，而是<strong>可執行的 API 規格</strong>。</p>
<hr>
<h2 id="測試命名的三種主要模式">測試命名的三種主要模式</h2>
<p>被測單元的契約大致分三類：「<strong>在某狀態下回傳什麼</strong>」「<strong>某操作會做什麼</strong>」「<strong>何時 throw / 失敗</strong>」——對應到測試命名也分三類 pattern。每類 pattern 的命名格式不同、負責驗證契約的不同切面。</p>
<h3 id="模式-1state-based狀態描述">模式 1：state-based（狀態描述）</h3>
<p>「在某個狀態下，呼叫 X 會回傳 / 變成什麼」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_null_when_user_not_found&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_empty_list_when_no_items_match&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_cached_value_on_second_call&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>適合：query / read-only 操作。</p>
<h3 id="模式-2scenario-based情境描述">模式 2：scenario-based（情境描述）</h3>
<p>「當某條件成立時，操作會做什麼」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;removes_item_when_quantity_reaches_zero&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;decreases_quantity_when_item_exists_with_quantity_above_one&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;updates_lastChangedItem_on_addItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;does_not_update_lastChangedItem_on_removeItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>適合：command / mutation 操作。注意 <code>does_not_X</code> 形式——<strong>negative assertion 也該寫進名字</strong>，這正是契約的一部分。</p>
<h3 id="模式-3failure-mode失敗模式描述">模式 3：failure-mode（失敗模式描述）</h3>
<p>「在某輸入 / 狀態下，會 throw / error / 失敗」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;throws_NotFoundException_when_id_does_not_exist&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;throws_StateError_when_called_after_dispose&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_error_when_network_unavailable&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>適合：error path、edge case。<strong>失敗模式是 doc 最容易漏寫的部分</strong>，但對 caller 最關鍵。</p>
<hr>
<h2 id="group-結構作為命名空間">Group 結構作為命名空間</h2>
<p>巢狀 group 提供了「主題 → 操作 → 情境」的階層命名空間，比扁平命名更易讀：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">group</span><span class="p">(</span><span class="s1">&#39;CartService&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;addItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;appends_when_item_not_in_cart&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;increments_quantity_when_same_item_exists&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;updates_lastChangedItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;removeItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;removes_when_item_exists&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;does_nothing_when_item_not_found&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;does_not_update_lastChangedItem&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="n">group</span><span class="p">(</span><span class="s1">&#39;decreaseQuantity&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;decreases_when_quantity_above_one&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">test</span><span class="p">(</span><span class="s1">&#39;removes_item_when_quantity_reaches_zero&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>讀者掃過 group 結構，立刻知道 <code>CartService</code> 對外提供哪些操作、每個操作有哪些行為承諾——<strong>這是這個 service 的 readable spec</strong>。</p>
<p>工具支援：好的 IDE / test runner 會把 group 結構顯示為樹狀，跑測試時的輸出也帶階層。把這個視覺結構利用好，測試 console 本身就是 doc 瀏覽器。</p>
<hr>
<h2 id="把-tests-當-readable-spec-的閱讀流程">把 tests 當 readable spec 的閱讀流程</h2>
<p>當你不確定一個 function 的行為時，閱讀順序通常是：</p>
<ol>
<li><strong>看簽章</strong> → 知道 what / takes / returns</li>
<li><strong>讀 doc</strong> → 知道契約、edge case</li>
<li><strong>看實作</strong> → 知道 how</li>
<li><strong>找測試</strong> → 看具體 case</li>
</ol>
<p>但如果測試命名做得好，<strong>順序可以對調</strong>：</p>
<ol>
<li>看簽章</li>
<li><strong>跳到對應 test file，掃 group + test names</strong> → 看 API 支援哪些 case、各 case 的承諾</li>
<li>不夠才回去讀 doc / 實作</li>
</ol>
<p>這個順序的優勢：</p>
<ul>
<li><strong>測試名是被驗證過的事實</strong>，doc 是聲明（可能 outdated）</li>
<li><strong>測試名涵蓋 edge case</strong>，比 doc 完整</li>
<li><strong>跳到測試只要一個快捷鍵</strong>（多數 IDE 有 &ldquo;Go to Test&rdquo; 命令）</li>
</ul>
<p>當團隊習慣這個閱讀順序，<strong>doc 寫多寫少的壓力就會減輕</strong>——很多 edge case 直接讓測試說明，doc 留給「測試也表達不了」的部分（業務動機、隱性需求）。</p>
<hr>
<h2 id="反模式">反模式</h2>
<h3 id="反模式-1test_-前綴--模糊主題">反模式 1：<code>test_</code> 前綴 + 模糊主題</h3>
<p><strong>正向概念</strong>：測試名字的每個 token 都該承載資訊——前綴或主題詞如果讀者一眼推不出「在驗什麼」、就是浪費 token budget。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：純 noise
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;test_user&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;test_user_2&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;test_user_creation&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// 正：說明具體行為
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;creates_user_with_default_role_when_role_omitted&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p><code>test_</code> 前綴是工具年代留下的習慣（早期某些 framework 靠它識別測試 method）；現代 framework 用 annotation / 函式簽章識別、前綴變成純 noise。模糊的主題（<code>test_user</code>、<code>test_creation</code>）等於沒命名——讀者必須跳進 body 才能分辨兩個 test 在驗什麼、命名的 doc 價值消失。</p>
<h3 id="反模式-2實作洩漏的命名">反模式 2：實作洩漏的命名</h3>
<p><strong>正向概念</strong>：測試驗的是<strong>對外可觀察的契約</strong>——換實作而契約沒變、測試應該繼續通過、命名也不該需要改。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：洩漏實作（用 hashmap、用 cache）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;uses_hashmap_for_lookup&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;caches_result_after_first_call&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 正：描述對外可觀察行為
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_value_in_O_1_for_existing_key&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;subsequent_calls_return_same_instance&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>命名洩漏實作後、重構（換 hashmap 為 trie、移除 cache 改用 lazy init）會逼迫測試一起改名——但對外行為其實沒變。一個良好的契約測試、應該在 codebase 大改造後仍能驗證「行為是否還是當初承諾的樣子」、命名洩漏實作會破壞這個性質。</p>
<h3 id="反模式-3描述怎麼做而非做什麼">反模式 3：描述「怎麼做」而非「做什麼」</h3>
<p><strong>正向概念</strong>：測試名描述「被測單元的契約」、test body 描述「測試怎麼寫」——分配給對應的位置、讀者跳到名字看契約、跳到 body 看細節。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：描述測試怎麼跑（過程）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;mocks_db_and_calls_findUser_then_asserts_result&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 正：描述被測 function 的行為
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_null_when_user_not_found&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>把「mocks_db_and_calls_X」寫進名字、讀者拿到的是「測試怎麼寫的過程」、不是「被測單元承諾什麼」——但讀 spec 想知道的是後者。「怎麼寫」放 test body、「驗證什麼契約」放名字、兩種讀者都得益。</p>
<h3 id="反模式-4assertion-style-命名">反模式 4：assertion-style 命名</h3>
<p><strong>正向概念</strong>：測試名是業務語義的入口、不是 assertion 框架的字面映射——讀者讀名字想推「業務上發生什麼」、不是「assert 用了哪個動詞」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：assertion 寫在名字
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;isFalse_when_disabled&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;equal_when_same_input&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 正：描述行為
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_false_when_feature_disabled&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;returns_same_result_for_equivalent_inputs&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p><code>isTrue</code>、<code>equal</code>、<code>isNotEmpty</code> 是 assertion 動詞、不是行為描述。讀者讀 <code>isFalse_when_disabled</code> 不知道「false」對應什麼業務語義（feature 關掉？user 不存在？status 失效？）——把業務語義寫進名字、讀者一眼就能 map 到實際情境。</p>
<h3 id="反模式-5用-numbering-取代命名">反模式 5：用 numbering 取代命名</h3>
<p><strong>正向概念</strong>：每個 test case 都有獨特的「驗什麼情境」、命名就是把那個情境寫出來。編號只負責「不重複」、不負責「能識別」——失去命名最關鍵的功能。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 反：靠編號區分
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_case_1&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_case_2&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_case_3&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// 正：編號變描述
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_appends_when_cart_empty&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_increments_when_same_item_exists&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;addItem_handles_null_customization&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span></span></span></code></pre></div><p>編號是「我懶得想名字」的訊號。讀者要跳進 test body 才能區分 case 1 跟 case 2 是什麼差別——失去測試命名的全部 doc 價值；CI 報告看到「<code>addItem_case_2</code> 失敗」也無從直接判斷哪個情境壞了。</p>
<hr>
<h2 id="邊界什麼時候測試名不適合當-spec">邊界：什麼時候測試名不適合當 spec</h2>
<p>「測試名是 spec 條目」是預設、<strong>但有些情境測試命名無法獨自承擔 doc 責任</strong>：</p>
<ul>
<li><strong>大量參數化 / property-based test</strong>：「對任意輸入 N、結果都 ≥ N」這類 invariant、命名只能寫概念名（<code>preserves_minimum</code>）、具體 input 範圍要靠 doc 或 generator 描述</li>
<li><strong>整合 / e2e test</strong>：跨多個系統的行為、命名常壓不下完整流程（「user_can_complete_checkout_with_loyalty_points_and_split_payment」）、要靠 setup / scenario doc 補上下文</li>
<li><strong>測試本身是業務動機的二次表達</strong>：例如 GDPR 合規規則、業務動機的詳細條款仍要寫在介面 doc / spec 文件、命名只負責「驗證點」</li>
<li><strong>內部行為對齊 vs 對外契約</strong>：私有 helper / internal worker 的測試命名不必當公開 spec、可以直接用實作詞彙（這時候命名價值是「regression 防護」而非「對外文件」）</li>
</ul>
<p>判斷標準：「讀者只看名字、能不能拿到他要的資訊？」答「能」就讓命名當 spec 用、答「不能」就把詳細上下文寫進 doc / scenario file、命名只當「定位錨點」。</p>
<hr>
<h2 id="給測試寫作的-checklist">給測試寫作的 checklist</h2>
<p>寫一個 test 之前，跑這個 checklist：</p>
<ul>
<li><input disabled="" type="checkbox"> <strong>名字能不能讓讀者不看 body 就知道驗證什麼？</strong> 不能 → 重命名</li>
<li><input disabled="" type="checkbox"> <strong>名字描述的是被測 function 的契約嗎？</strong> 不是（描述測試過程）→ 重寫</li>
<li><input disabled="" type="checkbox"> <strong>名字有沒有業務面詞彙？</strong> 沒有（只有 assertion 動詞）→ 加業務詞彙</li>
<li><input disabled="" type="checkbox"> <strong>同 group 下這個名字跟其他 test 有區辨度嗎？</strong> 沒有（靠編號）→ 加情境描述</li>
<li><input disabled="" type="checkbox"> <strong>這個行為契約是 doc 沒寫但這個 test 在驗的嗎？</strong> 是 → 太好了，這個 test 補了 doc 漏洞</li>
<li><input disabled="" type="checkbox"> <strong>這個 test 在驗實作細節嗎？</strong> 是 → 改成驗對外可觀察行為，否則重構必折斷</li>
</ul>
<hr>
<h2 id="trade-off測試名變長的代價">Trade-off：測試名變長的代價</h2>
<p>把測試當 doc 寫，名字會變長——<code>addItem_increments_quantity_when_same_item_exists_with_identical_customizations</code> 比 <code>test_add</code> 長 5 倍。</p>
<p>值得嗎？看你怎麼讀測試：</p>
<ul>
<li><strong>只看綠紅燈、不讀名字</strong> → 短名字便利</li>
<li><strong>把測試當 spec 讀</strong> → 長名字回收成本</li>
</ul>
<p>多數團隊低估「把測試當 spec 讀」的價值，因為這個習慣需要團隊一致才有效——一個人寫好命名，其他人不讀，回收不到。<strong>這是團隊習慣問題，不是個人偏好問題</strong>。要建立這個習慣，最好的切入點是：</p>
<ol>
<li><strong>新功能 PR 直接讀新 test 的名字判斷契約是否合理</strong>——把命名變成 review 的一環</li>
<li><strong>修 bug 時要求新增的 regression test 名字描述 bug 行為</strong>（例如 <code>does_not_double_charge_on_retry</code>）——這些名字本身是 incident 紀錄</li>
<li><strong>重構 PR 不允許改 test 名</strong>（除非是改名抓 bug 暴露的契約變動）——避免重構順手「整理」掉重要命名</li>
</ol>
<hr>
<h2 id="一句話-heuristic">一句話 heuristic</h2>
<p>把整個討論濃縮：</p>
<blockquote>
<p>測試名是「<strong>讀者跳到測試檔、不看 body 就能讀懂的 spec 條目</strong>」。</p></blockquote>
<p>寫測試名時想像一個讀者只會看到名字，他要能從名字推得：</p>
<ul>
<li>在驗哪個操作？</li>
<li>在哪個情境下？</li>
<li>期待什麼結果？</li>
</ul>
<p>三件事缺一不可。寫到名字過長覺得難寫——通常是被測 function 同時在做多件事，<strong>測試名長是設計訊號</strong>，先別急著縮名字，先想能不能拆 function。</p>
<hr>
<h2 id="收束測試命名是文件設計的一環">收束：測試命名是文件設計的一環</h2>
<p>回到開頭——測試是少數會自我驗證的文件。但這個性質<strong>只在你有意識利用時才有價值</strong>。把測試名寫成 <code>test_1</code>、<code>test_2</code>，你寫的是 regression 網，不是 doc。</p>
<p>把測試名寫成可讀 spec 條目，你寫的是同時包辦兩件事的東西：<strong>驗證 + 文件</strong>。這兩件事用同一份成本同時做完，是測試這個工具的最高槓桿用法。</p>
<p>把「<strong>這份 test file 是這個模組唯一的 doc、讀者夠不夠用？</strong>」當成命名的品質門檻——通過這個門檻的命名、自然就具備可讀 spec 的特性。</p>
]]></content:encoded></item></channel></rss>