<?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>Process on Tarragon</title><link>https://tarrragon.github.io/blog/tags/process/</link><description>Recent content in Process on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 02 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/process/index.xml" rel="self" type="application/rss+xml"/><item><title>程序、服務與狀態怎麼判</title><link>https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/</guid><description>&lt;p>判斷「某個東西現在是什麼狀態」——程式活著沒、服務由誰提供、螢幕鎖了沒、session 還在不在——是除錯裡最常做、也最常判錯的一步。判錯多半不是工具不對，是問錯了來源：用一個猜的名字去掃行程、用畫面有沒有反應去推服務狀態、用畫面上有沒有某個元素去斷定 session 狀態。這篇把幾個常見的狀態判斷，對到它們各自的權威來源與正確工具。&lt;/p>
&lt;p>底層的心法（讀權威狀態、不靠肉眼）見 &lt;a href="../diagnosis-read-authoritative-state/">診斷心法&lt;/a>，這篇是它在「程序 / 服務 / 狀態」這一類的具體招式。&lt;/p>
&lt;h2 id="程式活著沒比對正確的行程名">程式活著沒：比對正確的行程名&lt;/h2>
&lt;p>判斷一個程式在不在，行程表是權威來源，&lt;code>pgrep&lt;/code> / &lt;code>ps&lt;/code> 是對的工具，但成敗在於&lt;strong>比對正確的行程名&lt;/strong>（comm，行程表裡記的執行檔短名，可從 &lt;code>/proc/&amp;lt;pid&amp;gt;/comm&lt;/code> 看）。一個實際的坑：某個桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的可執行檔叫 &lt;code>quickshell&lt;/code>，但透過名為 &lt;code>qs&lt;/code> 的 symlink 啟動時，它在行程表裡的 comm 是 &lt;code>qs&lt;/code>。這時 &lt;code>pgrep quickshell&lt;/code> 找不到，很容易誤判成程式掛了、甚至誤觸「重啟」而引發更大的問題，實際上它以 &lt;code>qs&lt;/code> 這個名字好好跑著。&lt;/p>
&lt;p>可靠的做法：&lt;/p>
&lt;ul>
&lt;li>先確認實際的 comm 名：&lt;code>ps -eo pid,comm | grep -i &amp;lt;關鍵字&amp;gt;&lt;/code>，或看你啟動它的實際指令。&lt;/li>
&lt;li>用精確比對：&lt;code>pgrep -x &amp;lt;comm&amp;gt;&lt;/code>（&lt;code>-x&lt;/code> 要求完全相符），或 &lt;code>pgrep -af &amp;lt;pattern&amp;gt;&lt;/code> 連完整命令列一起比對，避免被 symlink 名 / 縮寫名騙。&lt;/li>
&lt;li>另一個 comm 的坑：kernel 把 comm 截在 15 字元（&lt;code>TASK_COMM_LEN&lt;/code>），名字超過 15 字的程式用 &lt;code>pgrep -x &amp;lt;完整長名&amp;gt;&lt;/code> 反而 miss——這時改用 &lt;code>pgrep -af &amp;lt;pattern&amp;gt;&lt;/code> 比對完整命令列。&lt;/li>
&lt;li>別用一個「你以為的名字」掃過去就下生死結論——行程表沒騙你，是查詢條件寫錯。&lt;/li>
&lt;/ul>
&lt;h3 id="進程活著--內部子系統活著">進程活著 ≠ 內部子系統活著&lt;/h3>
&lt;p>比對到了正確的 comm、&lt;code>pgrep&lt;/code> 也有輸出，只證明「這個進程存在」，不證明「它內部在正常運作」。有一類故障是進程好端端活著（&lt;code>pgrep&lt;/code> 找得到、STAT 是正常的 &lt;code>S&lt;/code>、在 &lt;code>poll&lt;/code> 等事件、CPU 不高），但它內部某個子系統已經 wedged——例如一個圖形 shell 的 QML scene 因為上游錯誤（渲染 pipeline 建失敗之類）某個物件沒建起來變 null，於是負責互動的模組全部失效。表現是 bar 還畫得出來、卻點不動，keybind 叫不出東西，但焦點視窗打字正常。這時 &lt;code>pgrep&lt;/code> 會騙你說「在跑」。&lt;/p>
&lt;p>這種情況權威來源不是行程表，是&lt;strong>程式自己的 log&lt;/strong>，而且這種 log 常常不在 &lt;code>journalctl&lt;/code>、也不在你猜的路徑，要用該程式專屬的 log 指令（例如某桌面 shell 的 &lt;code>&amp;lt;shell&amp;gt; -l&lt;/code>）。log 裡的 &lt;code>TypeError: Cannot read property 'X' of null&lt;/code> 這類訊息，才是「進程活著但子系統死了」的定案證據。另一個更精準的活性探針是程式的 &lt;strong>IPC 回不回真實狀態&lt;/strong>：正常時查詢會回傳資料、子系統死掉時回空——這比「進程在不在」可靠得多。判「進程活著到底有沒有在運作」時，讀它自己的 log 與 IPC，不是看 &lt;code>pgrep&lt;/code> 有沒有輸出。桌面 shell 的具體案例與恢復（讀 &lt;code>caelestia shell -l&lt;/code> 抓到 null 根因、重啟重建 scene）見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作&lt;/a> 的「畫得出來但互動死掉」場景。&lt;/p>
&lt;h2 id="服務由誰提供問註冊表">服務由誰提供：問註冊表&lt;/h2>
&lt;p>「某個系統服務現在由哪個程式在提供」，權威來源是服務註冊，不是畫面。桌面服務多半註冊在 &lt;strong>D-Bus&lt;/strong>（Linux 桌面的行程間訊息匯流排）上：一個服務用一個名字掛在上面，而&lt;strong>同一個名字同一時間只能被一個行程擁有&lt;/strong>。以桌面通知為例，&lt;code>org.freedesktop.Notifications&lt;/code> 這個 D-Bus 名同一時間只有一個擁有者——兩個通知 daemon（例如 mako 跟某個桌面 shell 內建的通知服務）不能共存，誰先註冊誰佔著，後者只能等前者退出。&lt;/p>
&lt;p>想知道現在是誰接管，查註冊表而不是送一則通知看畫面：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 查 org.freedesktop.Notifications 目前被哪個連線擁有&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nv">owner&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> org.freedesktop.DBus GetNameOwner s org.freedesktop.Notifications &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $2}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;&amp;#34;&amp;#39;&lt;/span>&lt;span class="k">)&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"># 把那個連線換算成 PID，再看行程名&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nv">pid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> org.freedesktop.DBus GetConnectionUnixProcessID s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$owner&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $2}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">ps -o &lt;span class="nv">comm&lt;/span>&lt;span class="o">=&lt;/span> -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$pid&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>停掉舊 daemon 前擁有者是舊的、停掉後換成新的，就確認接管成功。這比「送通知看畫面有沒有跳」可靠——畫面沒跳可能是勿擾模式吃掉、可能根本沒送出，畫面反應不等於服務歸屬。切換兩個搶同一服務名的 daemon 時，這也解釋了為什麼「新的裝了卻沒作用」：舊的還佔著名字，新的靜默註冊失敗（通常只在它的 log 留一行 warning），得先停掉舊的。&lt;/p></description><content:encoded><![CDATA[<p>判斷「某個東西現在是什麼狀態」——程式活著沒、服務由誰提供、螢幕鎖了沒、session 還在不在——是除錯裡最常做、也最常判錯的一步。判錯多半不是工具不對，是問錯了來源：用一個猜的名字去掃行程、用畫面有沒有反應去推服務狀態、用畫面上有沒有某個元素去斷定 session 狀態。這篇把幾個常見的狀態判斷，對到它們各自的權威來源與正確工具。</p>
<p>底層的心法（讀權威狀態、不靠肉眼）見 <a href="../diagnosis-read-authoritative-state/">診斷心法</a>，這篇是它在「程序 / 服務 / 狀態」這一類的具體招式。</p>
<h2 id="程式活著沒比對正確的行程名">程式活著沒：比對正確的行程名</h2>
<p>判斷一個程式在不在，行程表是權威來源，<code>pgrep</code> / <code>ps</code> 是對的工具，但成敗在於<strong>比對正確的行程名</strong>（comm，行程表裡記的執行檔短名，可從 <code>/proc/&lt;pid&gt;/comm</code> 看）。一個實際的坑：某個桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的可執行檔叫 <code>quickshell</code>，但透過名為 <code>qs</code> 的 symlink 啟動時，它在行程表裡的 comm 是 <code>qs</code>。這時 <code>pgrep quickshell</code> 找不到，很容易誤判成程式掛了、甚至誤觸「重啟」而引發更大的問題，實際上它以 <code>qs</code> 這個名字好好跑著。</p>
<p>可靠的做法：</p>
<ul>
<li>先確認實際的 comm 名：<code>ps -eo pid,comm | grep -i &lt;關鍵字&gt;</code>，或看你啟動它的實際指令。</li>
<li>用精確比對：<code>pgrep -x &lt;comm&gt;</code>（<code>-x</code> 要求完全相符），或 <code>pgrep -af &lt;pattern&gt;</code> 連完整命令列一起比對，避免被 symlink 名 / 縮寫名騙。</li>
<li>另一個 comm 的坑：kernel 把 comm 截在 15 字元（<code>TASK_COMM_LEN</code>），名字超過 15 字的程式用 <code>pgrep -x &lt;完整長名&gt;</code> 反而 miss——這時改用 <code>pgrep -af &lt;pattern&gt;</code> 比對完整命令列。</li>
<li>別用一個「你以為的名字」掃過去就下生死結論——行程表沒騙你，是查詢條件寫錯。</li>
</ul>
<h3 id="進程活著--內部子系統活著">進程活著 ≠ 內部子系統活著</h3>
<p>比對到了正確的 comm、<code>pgrep</code> 也有輸出，只證明「這個進程存在」，不證明「它內部在正常運作」。有一類故障是進程好端端活著（<code>pgrep</code> 找得到、STAT 是正常的 <code>S</code>、在 <code>poll</code> 等事件、CPU 不高），但它內部某個子系統已經 wedged——例如一個圖形 shell 的 QML scene 因為上游錯誤（渲染 pipeline 建失敗之類）某個物件沒建起來變 null，於是負責互動的模組全部失效。表現是 bar 還畫得出來、卻點不動，keybind 叫不出東西，但焦點視窗打字正常。這時 <code>pgrep</code> 會騙你說「在跑」。</p>
<p>這種情況權威來源不是行程表，是<strong>程式自己的 log</strong>，而且這種 log 常常不在 <code>journalctl</code>、也不在你猜的路徑，要用該程式專屬的 log 指令（例如某桌面 shell 的 <code>&lt;shell&gt; -l</code>）。log 裡的 <code>TypeError: Cannot read property 'X' of null</code> 這類訊息，才是「進程活著但子系統死了」的定案證據。另一個更精準的活性探針是程式的 <strong>IPC 回不回真實狀態</strong>：正常時查詢會回傳資料、子系統死掉時回空——這比「進程在不在」可靠得多。判「進程活著到底有沒有在運作」時，讀它自己的 log 與 IPC，不是看 <code>pgrep</code> 有沒有輸出。桌面 shell 的具體案例與恢復（讀 <code>caelestia shell -l</code> 抓到 null 根因、重啟重建 scene）見 <a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a> 的「畫得出來但互動死掉」場景。</p>
<h2 id="服務由誰提供問註冊表">服務由誰提供：問註冊表</h2>
<p>「某個系統服務現在由哪個程式在提供」，權威來源是服務註冊，不是畫面。桌面服務多半註冊在 <strong>D-Bus</strong>（Linux 桌面的行程間訊息匯流排）上：一個服務用一個名字掛在上面，而<strong>同一個名字同一時間只能被一個行程擁有</strong>。以桌面通知為例，<code>org.freedesktop.Notifications</code> 這個 D-Bus 名同一時間只有一個擁有者——兩個通知 daemon（例如 mako 跟某個桌面 shell 內建的通知服務）不能共存，誰先註冊誰佔著，後者只能等前者退出。</p>
<p>想知道現在是誰接管，查註冊表而不是送一則通知看畫面：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 查 org.freedesktop.Notifications 目前被哪個連線擁有</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">owner</span><span class="o">=</span><span class="k">$(</span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  org.freedesktop.DBus GetNameOwner s org.freedesktop.Notifications <span class="p">|</span> awk <span class="s1">&#39;{print $2}&#39;</span> <span class="p">|</span> tr -d <span class="s1">&#39;&#34;&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 把那個連線換算成 PID，再看行程名</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">pid</span><span class="o">=</span><span class="k">$(</span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  org.freedesktop.DBus GetConnectionUnixProcessID s <span class="s2">&#34;</span><span class="nv">$owner</span><span class="s2">&#34;</span> <span class="p">|</span> awk <span class="s1">&#39;{print $2}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">ps -o <span class="nv">comm</span><span class="o">=</span> -p <span class="s2">&#34;</span><span class="nv">$pid</span><span class="s2">&#34;</span></span></span></code></pre></div><p>停掉舊 daemon 前擁有者是舊的、停掉後換成新的，就確認接管成功。這比「送通知看畫面有沒有跳」可靠——畫面沒跳可能是勿擾模式吃掉、可能根本沒送出，畫面反應不等於服務歸屬。切換兩個搶同一服務名的 daemon 時，這也解釋了為什麼「新的裝了卻沒作用」：舊的還佔著名字，新的靜默註冊失敗（通常只在它的 log 留一行 warning），得先停掉舊的。</p>
<h2 id="桌面-session-有沒有被鎖認清是哪一層的鎖">桌面 session 有沒有被鎖：認清是哪一層的鎖</h2>
<p>判斷一個圖形 session 有沒有被鎖，最容易被畫面帶偏，因為「畫面上有密碼框」很有說服力、卻不等於 session 真的被鎖（現代桌面 shell 的儀表板常內嵌鎖屏樣式的 widget）。而且鎖有不同層，查錯層會得到誤導的答案。</p>
<p>關鍵是分清兩種鎖：</p>
<ul>
<li><strong>logind 層的鎖</strong>：systemd 登入管理的 session 鎖，權威狀態是 <code>loginctl show-session &lt;id&gt; -p LockedHint</code>。</li>
<li><strong>Wayland 合成器層的鎖</strong>：走 <code>ext-session-lock</code> 協議、由<strong>合成器</strong>（compositor，Wayland 下負責把各視窗合成到螢幕、管輸入輸出的核心程式，約當 X11 時代的視窗管理器加顯示伺服器；Hyprland、Sway 等都是）管的鎖，跟 logind 是獨立機制。這種鎖 <code>loginctl</code> 的 <code>LockedHint</code> <strong>查不到</strong>——不是沒鎖，是查錯層。（用 GNOME / KDE 的鎖屏走的機制不同，以下的 <code>ext-session-lock</code> 判法與復原針對 wlroots 系的 Wayland 合成器。）</li>
</ul>
<p>所以「<code>loginctl</code> 沒有 <code>LockedHint</code>、<code>pgrep</code> 找不到獨立鎖屏程式」不足以斷定「沒鎖」：合成器層的鎖不歸 logind、而鎖屏畫面可能由 shell 主程式在自己行程內畫（沒有獨立可執行檔可抓）。這種情況真正的權威來源是那個 shell 自己的 log（有沒有載入鎖屏模組、idle 計時器有沒有觸發鎖定），或直接看 compositor 的 session-lock 狀態。判鎖看合成器 / shell 的 log，不是 <code>loginctl</code>、更不是畫面有沒有密碼框。</p>
<h3 id="鎖屏程式死掉造成的死局與復原">鎖屏程式死掉造成的死局與復原</h3>
<p><code>ext-session-lock</code> 有一個安全設計：持鎖的鎖屏程式若在鎖定狀態下崩潰 / 被中止，compositor <strong>會保持鎖定</strong>、不會因為鎖屏程式沒了就解鎖（否則殺掉鎖屏程式就成了繞過鎖的漏洞）。表現是畫面卡在「鎖屏程式已死」的安全提示。復原要從另一個 VT 或 SSH 用 <code>hyprctl keyword misc:allow_session_lock_restore 1</code> 允許新鎖屏 client 接管、再 <code>hyprctl dispatch exec hyprlock</code> 起一個接管後輸密碼解鎖。完整機制、兩層鎖的關係、各 compositor 的差異，見 <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Wayland Session Lock 卡</a>。</p>
<p>診斷紀律：<strong>測鎖屏、或 <code>pkill</code> 一個持鎖的鎖屏程式時，要預期它把 session 卡在鎖定——這是協議的安全設計，不是 bug。</strong> 自動化 / 無人值守流程尤其要避免在持鎖狀態下殺鎖屏程式。</p>
<h2 id="終端機多工器的-session-還在不在">終端機多工器的 session 還在不在</h2>
<p>用 zellij / tmux 這類多工器跑遠端長任務時，判斷「重連後那個 session 還在不在」的權威來源是多工器自己的 session 列表，不是「我 SSH 斷了所以應該還在吧」的假設。<code>zellij ls</code>（或 <code>tmux ls</code>）會列出 session 與狀態：多工器是常駐在遠端的程序，SSH 斷不影響它，所以只要那台機器沒重開，<code>attach</code> 就能接回去；但如果機器重開過、或那個 session 因為資源不足（例如磁碟滿觸發的連鎖）被殺，列表會顯示它已 <code>EXITED</code> / 不存在，這種接不回去。</p>
<p>這裡有個順序上的紀律：<strong>當一個 session 可能已經死掉、而它裡面跑的任務有你在意的產出時，先確認產出有沒有被安全保存，再處理 session。</strong> 例如任務是在改 git repo，先 <code>git -C &lt;repo&gt; status</code> 跟 <code>git log @{u}..</code>（本地有、遠端沒有的 commit）確認有沒有沒推送的東西、把該推的推掉，再去 <code>zellij delete</code> 清死 session。搞反順序、先清了 session，可能連帶失去唯一還記得那些改動的地方。權威狀態（git 的推送狀態、多工器的 session 列表）先讀清楚，再動手。</p>
<h2 id="判讀路由">判讀路由</h2>
<ul>
<li>判程式活著 → <code>pgrep -x &lt;正確 comm&gt;</code> / <code>pgrep -af &lt;pattern&gt;</code>，先確認實際 comm 名，別用猜的名字。</li>
<li>判進程活著但「有沒有在運作」→ 讀程式自己的 log（可能要用它專屬的 log 指令、不在 journalctl）+ 它的 IPC 回不回真實狀態，不是看 <code>pgrep</code> 有輸出就當正常。</li>
<li>判服務歸誰 → <code>busctl</code> 查 D-Bus name 擁有者 → 換算 PID → comm，不看畫面反應。</li>
<li>判 session 鎖沒鎖 → 分清 logind 層（<code>loginctl LockedHint</code>）vs 合成器層（<code>ext-session-lock</code>，看 compositor / shell log），不看畫面有沒有密碼框。</li>
<li>鎖屏程式死掉卡住 → <code>allow_session_lock_restore</code> + 重起鎖屏程式接管解鎖。</li>
<li>判多工器 session 存活 → <code>zellij ls</code> / <code>tmux ls</code>；可能已死且有在意的產出時，先確認產出已保存 / 已推送再清 session。</li>
</ul>
<p>判不準時，<a href="../diagnosis-read-authoritative-state/">診斷心法</a> 的四步（描述症狀、定位權威來源、用對工具讀、矛盾時信權威）是通用的回退。</p>
]]></content:encoded></item><item><title>高 ROI 無外部觸發的工作會被結構性跳過</title><link>https://tarrragon.github.io/blog/report/external-trigger-for-high-roi-work/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/external-trigger-for-high-roi-work/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;blockquote>
&lt;p>工作有兩個獨立維度：ROI 高低 × 是否有外部觸發。&lt;/p>&lt;/blockquote>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>ROI / 觸發&lt;/th>
 &lt;th>有外部觸發&lt;/th>
 &lt;th>沒外部觸發&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>高 ROI&lt;/strong>&lt;/td>
 &lt;td>順利做（happy path）&lt;/td>
 &lt;td>&lt;strong>被結構性跳過&lt;/strong>（本卡焦點）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>低 ROI&lt;/strong>&lt;/td>
 &lt;td>該砍掉、不該做&lt;/td>
 &lt;td>自然不做（也對）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「&lt;strong>高 ROI + 沒外部觸發&lt;/strong>」是個結構性陷阱 — 知道該做、做了有大回報、但永遠不做。靠「我下次記得」不可行。修法是&lt;strong>結構性對策&lt;/strong>：把外部觸發補上。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼靠紀律不可行">為什麼靠紀律不可行&lt;/h2>
&lt;h3 id="之後做是個謊言共同結構">「之後做」是個謊言（共同結構）&lt;/h3>
&lt;p>&lt;a href="../ease-of-writing-vs-intent-alignment/">#67 我等下會 refactor 是個謊言&lt;/a> 已經點到一個面向。把它推廣：&lt;/p>
&lt;p>「之後做 X」這個 plan 在 X 屬於「高 ROI + 無觸發」時、預期完成率接近 0。不是個人意志問題、是結構問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工作觸發來源&lt;/th>
 &lt;th>「之後做」的執行率&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>客戶來信催&lt;/td>
 &lt;td>~95%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bug 卡死流程&lt;/td>
 &lt;td>~95%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Calendar reminder&lt;/td>
 &lt;td>~70%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sprint planning&lt;/td>
 &lt;td>~60%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>自己記下的 TODO&lt;/td>
 &lt;td>~30%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>「下次有空我做」&lt;/td>
 &lt;td>~5%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>往下走、外部觸發越弱、執行率越低。最弱的「下次有空我做」≈ 0% — 因為「下次」永遠是「現在」、「現在」永遠有更急的事。&lt;/p>
&lt;h3 id="為什麼結構性不是動機問題">為什麼結構性、不是動機問題&lt;/h3>
&lt;p>「沒外部觸發」 = 沒人催、沒 deadline、沒 alarm、沒 PR review 提醒。腦中有 working memory 限制、優先處理「正在叫」的事。&lt;strong>「叫」這個動作只有外部能做&lt;/strong> — 自己對自己叫沒用（因為「自己叫自己時」跟「自己接受自己叫時」是同個 context）。&lt;/p>
&lt;p>這跟意志力、自律、責任感無關 — 即使最自律的人、面對「沒人催的高 ROI 工作」，執行率也大幅下降。靠紀律 = 預期失敗、然後責怪自己。&lt;/p>
&lt;hr>
&lt;h2 id="多面向高-roi--無觸發的工作清單">多面向：高 ROI + 無觸發的工作清單&lt;/h2>
&lt;p>每一條都對應某張既有卡的具體展現：&lt;/p>
&lt;h3 id="寫程式類">寫程式類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Refactor（沒功能壓力）&lt;/strong> — &lt;a href="../ease-of-writing-vs-intent-alignment/">#67&lt;/a>&lt;/li>
&lt;li>&lt;strong>Test-first 的 RED 階段（修完才補測試）&lt;/strong> — &lt;a href="../test-first-red-before-green/">#69&lt;/a>&lt;/li>
&lt;li>&lt;strong>Checkpoint 1（列使用者意圖完整集）&lt;/strong> — &lt;a href="../verification-timeline-checkpoints/">#68&lt;/a>&lt;/li>
&lt;li>&lt;strong>Ship 前 E2E case 設計&lt;/strong> — &lt;a href="../verification-timeline-checkpoints/">#68&lt;/a>&lt;/li>
&lt;li>&lt;strong>Code review feedback 的 follow-up&lt;/strong>（reviewer 留 comment、作者回「之後改」）&lt;/li>
&lt;/ul>
&lt;h3 id="維護類">維護類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Migration cleanup（feature flag 拔除、舊 path 砍掉）&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Deprecated 程式碼移除&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Dependency upgrade（沒 breaking 但該升）&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Performance regression 修復（測量上有但使用者沒抱怨）&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h3 id="文件類">文件類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>API doc / README 更新&lt;/strong>&lt;/li>
&lt;li>&lt;strong>事後檢討卡片寫入&lt;/strong>（這個 cards-skills 系統就是 case — 沒 user 提醒就不會做）&lt;/li>
&lt;li>&lt;strong>Decision log / ADR&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h3 id="監控類">監控類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Setup observability / log monitor（&lt;a href="../verification-timeline-checkpoints/">#68&lt;/a> Checkpoint 4）&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Alert 規則 review&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Dashboard 維護&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h3 id="知識類">知識類&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Onboarding doc 更新&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Post-mortem 寫完發出去&lt;/strong>&lt;/li>
&lt;li>&lt;strong>跨團隊 share session&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>共通結構&lt;/strong>：每一項都「知道該做、做了有大回報、沒人催就不做」。即使是寫過卡片教自己原則的人（meta-level dogfooding 失敗）也一樣會跳過。&lt;/p>
&lt;hr>
&lt;h2 id="修法結構性對策的五個層級">修法：結構性對策的五個層級&lt;/h2>
&lt;p>從弱到強：&lt;/p>
&lt;h3 id="l1個人紀律最弱不可行">L1：個人紀律（最弱、不可行）&lt;/h3>
&lt;p>「我下次記得」「我會自律」 — 已經證明 ≈ 0% 執行率。不該寫進 plan。&lt;/p>
&lt;h3 id="l2自我排程弱">L2：自我排程（弱）&lt;/h3>
&lt;p>「每週五下午 refactor 1 小時」「每個月初 review TODO」。比 L1 強、但仍依賴自己當下不分心、不被「更急」的事拉走。執行率約 30-50%。&lt;/p>
&lt;h3 id="l3外部工具觸發中-強">L3：外部工具觸發（中-強）&lt;/h3>
&lt;p>把觸發外化到工具：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CI / pre-commit hook&lt;/strong>：commit test file 自動提醒「跑過 RED 嗎」&lt;/li>
&lt;li>&lt;strong>Scheduled scripts&lt;/strong>：cron job 跑 lint / dep audit / migration cleanup detector&lt;/li>
&lt;li>&lt;strong>Calendar event&lt;/strong>：固定時間、有 alarm&lt;/li>
&lt;li>&lt;strong>PR template&lt;/strong>：強制填「Checkpoint 1 列了哪些 case」&lt;/li>
&lt;/ul>
&lt;p>工具不會忘、不會拖、不會選擇性執行。執行率 80-95%。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<blockquote>
<p>工作有兩個獨立維度：ROI 高低 × 是否有外部觸發。</p></blockquote>
<table>
  <thead>
      <tr>
          <th>ROI / 觸發</th>
          <th>有外部觸發</th>
          <th>沒外部觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>高 ROI</strong></td>
          <td>順利做（happy path）</td>
          <td><strong>被結構性跳過</strong>（本卡焦點）</td>
      </tr>
      <tr>
          <td><strong>低 ROI</strong></td>
          <td>該砍掉、不該做</td>
          <td>自然不做（也對）</td>
      </tr>
  </tbody>
</table>
<p>「<strong>高 ROI + 沒外部觸發</strong>」是個結構性陷阱 — 知道該做、做了有大回報、但永遠不做。靠「我下次記得」不可行。修法是<strong>結構性對策</strong>：把外部觸發補上。</p>
<hr>
<h2 id="為什麼靠紀律不可行">為什麼靠紀律不可行</h2>
<h3 id="之後做是個謊言共同結構">「之後做」是個謊言（共同結構）</h3>
<p><a href="../ease-of-writing-vs-intent-alignment/">#67 我等下會 refactor 是個謊言</a> 已經點到一個面向。把它推廣：</p>
<p>「之後做 X」這個 plan 在 X 屬於「高 ROI + 無觸發」時、預期完成率接近 0。不是個人意志問題、是結構問題：</p>
<table>
  <thead>
      <tr>
          <th>工作觸發來源</th>
          <th>「之後做」的執行率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客戶來信催</td>
          <td>~95%</td>
      </tr>
      <tr>
          <td>Bug 卡死流程</td>
          <td>~95%</td>
      </tr>
      <tr>
          <td>Calendar reminder</td>
          <td>~70%</td>
      </tr>
      <tr>
          <td>Sprint planning</td>
          <td>~60%</td>
      </tr>
      <tr>
          <td>自己記下的 TODO</td>
          <td>~30%</td>
      </tr>
      <tr>
          <td>「下次有空我做」</td>
          <td>~5%</td>
      </tr>
  </tbody>
</table>
<p>往下走、外部觸發越弱、執行率越低。最弱的「下次有空我做」≈ 0% — 因為「下次」永遠是「現在」、「現在」永遠有更急的事。</p>
<h3 id="為什麼結構性不是動機問題">為什麼結構性、不是動機問題</h3>
<p>「沒外部觸發」 = 沒人催、沒 deadline、沒 alarm、沒 PR review 提醒。腦中有 working memory 限制、優先處理「正在叫」的事。<strong>「叫」這個動作只有外部能做</strong> — 自己對自己叫沒用（因為「自己叫自己時」跟「自己接受自己叫時」是同個 context）。</p>
<p>這跟意志力、自律、責任感無關 — 即使最自律的人、面對「沒人催的高 ROI 工作」，執行率也大幅下降。靠紀律 = 預期失敗、然後責怪自己。</p>
<hr>
<h2 id="多面向高-roi--無觸發的工作清單">多面向：高 ROI + 無觸發的工作清單</h2>
<p>每一條都對應某張既有卡的具體展現：</p>
<h3 id="寫程式類">寫程式類</h3>
<ul>
<li><strong>Refactor（沒功能壓力）</strong> — <a href="../ease-of-writing-vs-intent-alignment/">#67</a></li>
<li><strong>Test-first 的 RED 階段（修完才補測試）</strong> — <a href="../test-first-red-before-green/">#69</a></li>
<li><strong>Checkpoint 1（列使用者意圖完整集）</strong> — <a href="../verification-timeline-checkpoints/">#68</a></li>
<li><strong>Ship 前 E2E case 設計</strong> — <a href="../verification-timeline-checkpoints/">#68</a></li>
<li><strong>Code review feedback 的 follow-up</strong>（reviewer 留 comment、作者回「之後改」）</li>
</ul>
<h3 id="維護類">維護類</h3>
<ul>
<li><strong>Migration cleanup（feature flag 拔除、舊 path 砍掉）</strong></li>
<li><strong>Deprecated 程式碼移除</strong></li>
<li><strong>Dependency upgrade（沒 breaking 但該升）</strong></li>
<li><strong>Performance regression 修復（測量上有但使用者沒抱怨）</strong></li>
</ul>
<h3 id="文件類">文件類</h3>
<ul>
<li><strong>API doc / README 更新</strong></li>
<li><strong>事後檢討卡片寫入</strong>（這個 cards-skills 系統就是 case — 沒 user 提醒就不會做）</li>
<li><strong>Decision log / ADR</strong></li>
</ul>
<h3 id="監控類">監控類</h3>
<ul>
<li><strong>Setup observability / log monitor（<a href="../verification-timeline-checkpoints/">#68</a> Checkpoint 4）</strong></li>
<li><strong>Alert 規則 review</strong></li>
<li><strong>Dashboard 維護</strong></li>
</ul>
<h3 id="知識類">知識類</h3>
<ul>
<li><strong>Onboarding doc 更新</strong></li>
<li><strong>Post-mortem 寫完發出去</strong></li>
<li><strong>跨團隊 share session</strong></li>
</ul>
<p><strong>共通結構</strong>：每一項都「知道該做、做了有大回報、沒人催就不做」。即使是寫過卡片教自己原則的人（meta-level dogfooding 失敗）也一樣會跳過。</p>
<hr>
<h2 id="修法結構性對策的五個層級">修法：結構性對策的五個層級</h2>
<p>從弱到強：</p>
<h3 id="l1個人紀律最弱不可行">L1：個人紀律（最弱、不可行）</h3>
<p>「我下次記得」「我會自律」 — 已經證明 ≈ 0% 執行率。不該寫進 plan。</p>
<h3 id="l2自我排程弱">L2：自我排程（弱）</h3>
<p>「每週五下午 refactor 1 小時」「每個月初 review TODO」。比 L1 強、但仍依賴自己當下不分心、不被「更急」的事拉走。執行率約 30-50%。</p>
<h3 id="l3外部工具觸發中-強">L3：外部工具觸發（中-強）</h3>
<p>把觸發外化到工具：</p>
<ul>
<li><strong>CI / pre-commit hook</strong>：commit test file 自動提醒「跑過 RED 嗎」</li>
<li><strong>Scheduled scripts</strong>：cron job 跑 lint / dep audit / migration cleanup detector</li>
<li><strong>Calendar event</strong>：固定時間、有 alarm</li>
<li><strong>PR template</strong>：強制填「Checkpoint 1 列了哪些 case」</li>
</ul>
<p>工具不會忘、不會拖、不會選擇性執行。執行率 80-95%。</p>
<h3 id="l4團隊流程強">L4：團隊流程（強）</h3>
<p>把觸發外化到別人：</p>
<ul>
<li><strong>Pair programming</strong>：另一個人在旁邊、會問「為什麼跳過 X」</li>
<li><strong>Code review block</strong>：reviewer 不通過 PR 直到 X 完成</li>
<li><strong>Standup commitment</strong>：公開講出「我這週要修 X」、隔天會被問</li>
<li><strong>Retro action items</strong>：團隊紀錄 + 追蹤、不個人擁有</li>
</ul>
<p>執行率 90-99%。</p>
<h3 id="l5結構性不可能最強">L5：結構性不可能（最強）</h3>
<p>讓不做 X 變成 ship 不出去：</p>
<ul>
<li><strong>Tests required</strong>：CI fail 不能 merge</li>
<li><strong>Build fails on stale doc</strong>：lint 規則檢查 doc 跟 code 同步</li>
<li><strong>Feature flag 自動 expire</strong>：超過某時間、flag 被自動移除</li>
<li><strong>Linter 禁用 deprecated API</strong>：用了就 build 錯</li>
</ul>
<p>100% 執行率（系統強制）。代價：建立成本高、要團隊認可。</p>
<p>選擇法則：<strong>先看哪個層級剛好夠</strong>、不要用 L5 解 L3 能解的問題（過度工程）、也不要用 L1 解 L4 才能解的問題（會失敗）。</p>
<hr>
<h2 id="想到就動手是次優不是最優">「想到就動手」是次優、不是最優</h2>
<p>直覺反應是「想到該做就立刻做」、避免拖延。這在「想到時剛好沒手邊事」可行、但實際多半「想到時手邊有事」 — 變成中斷當前工作、context switch 高昂。</p>
<p>更穩定的策略：<strong>把想到的東西塞進已存在的觸發機制</strong>：</p>
<ul>
<li>想到「這個重複了該抽 helper」 → 開 issue / TODO 給下次 refactor session</li>
<li>想到「這個 case 沒測」 → 加進 PR template 的 Checkpoint 1 list</li>
<li>想到「這個 doc 過時了」 → 打開 doc 在 commit 寫 <code>// TODO: 更新 X</code></li>
</ul>
<p>「動手」的時機由觸發決定、不由「想到」決定。<strong>想到 = 觸發機制的 input、不是執行的 trigger</strong>。</p>
<hr>
<h2 id="不該套用本原則的情境">不該套用本原則的情境</h2>
<p>「高 ROI + 無觸發 = 結構性跳過」原則在多數情境成立、但有合理例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不該套用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純探索 / 興趣專案</td>
          <td>沒 ROI 概念、做了爽就好、不需要結構性對策</td>
      </tr>
      <tr>
          <td>一次性極小工作</td>
          <td>5 分鐘內完成、加 trigger 反而成本高</td>
      </tr>
      <tr>
          <td>緊急 incident</td>
          <td>已有最強觸發（系統壞了）、不需額外結構</td>
      </tr>
      <tr>
          <td>還沒穩定的探索期</td>
          <td>規則還在演化、結構性對策可能會卡死探索</td>
      </tr>
      <tr>
          <td>學習新技術 / 練習</td>
          <td>自己選、沒外部 ROI 衡量、跳過也不損失</td>
      </tr>
  </tbody>
</table>
<p>四類共同特徵：<strong>「外部觸發」這個變數已經有解或不存在</strong> — 本原則建立在「沒觸發 = 跳過」上、有觸發或不需要時自然不適用。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>跟本卡的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a></td>
          <td>#67 是本卡在「寫程式當下選哪條路」面向的展現 — 對齊 = 高 ROI 但無觸發</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>#68 的「Ship 前 / Checkpoint 1 結構性偏差」是本卡在驗收動作的展現</td>
      </tr>
      <tr>
          <td><a href="../test-first-red-before-green/">#69 Test-First</a></td>
          <td>RED 階段被跳過 = 本卡在測試協議的展現</td>
      </tr>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>失敗訊號需要被「外部承認」才能觸發轉折 — 跟本卡共骨</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>本卡的 ceiling — L5 hook 只擋字面、行為錯誤需要 L4 review / multi-pass spiral、不是「再寫一條 hook 規則」</td>
      </tr>
  </tbody>
</table>
<p>本卡是 meta-#67/#68/#69 — 把「為什麼這些動作會被跳過」抽出來、答案是「沒外部觸發 + 靠紀律失敗 = 結構性跳過」。三張卡的修法都是「補外部觸發」、不是「自己更努力」。</p>
<hr>
<h2 id="對應的實作篇--系統建設">對應的實作篇 / 系統建設</h2>
<p>把本原則套用到本系統的具體 case：</p>
<ul>
<li><strong><code>make verify-red-green</code> script</strong>（<a href="../test-first-red-before-green/">#69</a>）— L3 工具觸發、把 retrospective 流程從文字協議升級成可執行 target</li>
<li><strong>playwright CI workflow</strong>（push / PR 觸發）— L5 結構性、test 不過就無法 merge</li>
<li><strong>md-check workflow</strong> — L5 強制、卡片格式不對 build fail</li>
<li><strong>本卡誕生過程</strong> — User 提問是 L4 外部觸發、把「該回頭抽 meta」變成有壓力的動作（不然不會做）</li>
</ul>
<p>每一個都是「把高 ROI + 無觸發的工作、補上對應層級的觸發」。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan 含「之後我會 X」</td>
          <td>是 L1 紀律、預期失敗、改成 L3+ 觸發</td>
      </tr>
      <tr>
          <td>TODO list 累積 30+ 項、半年沒減少</td>
          <td>觸發機制壞了、不是「太忙」</td>
      </tr>
      <tr>
          <td>某類重要工作（refactor / doc / monitor）長期沒做</td>
          <td>沒外部觸發、補 L3-L5</td>
      </tr>
      <tr>
          <td>自己責怪「我又拖延了」</td>
          <td>結構問題不是個人問題、停止責怪、改機制</td>
      </tr>
      <tr>
          <td>同團隊不同人做同類工作的執行率差很多</td>
          <td>個別人差是表象、機制設計問題（流程不一致）</td>
      </tr>
      <tr>
          <td>某個 lint / CI rule 改完所有人都自動跟上</td>
          <td>L5 對策成功、適合複用到其他類似工作</td>
      </tr>
      <tr>
          <td>「想到就立刻做」打斷正在做的事</td>
          <td>動作該由觸發排程、不由 thoughts 觸發</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：高 ROI 但無外部觸發的工作 = 結構性跳過、不是個人問題。修法是把觸發外化（工具 / 流程 / 結構）、不是「我下次記得」。「之後我會 X」是 plan-level 警訊、應該轉成「X 會被 Y 觸發」的具體機制。</p>
]]></content:encoded></item><item><title>分批 ship：低風險可見價值先行、結構性下輪</title><link>https://tarrragon.github.io/blog/report/incremental-shipping-criteria/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/incremental-shipping-criteria/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>寫到「該 ship 哪些」時、預設&lt;strong>分批&lt;/strong>：把 changes 沿三軸切 — &lt;strong>使用者可見性高 + 風險低 + 驗證簡單&lt;/strong> 的先 ship、&lt;strong>結構性 + 風險高 + 需驗證&lt;/strong> 的下輪。對抗「都做完才能 ship」的整體性衝動。&lt;/p>
&lt;p>分批的真正價值：&lt;strong>降低每次 review 的 cognitive load + 加速使用者拿到價值 + 讓回退單位更小&lt;/strong>。整批 ship 的代價是 review 變慢、bug 排查面變大、出問題回退要拖整批。&lt;/p>
&lt;hr>
&lt;h2 id="三軸切分">三軸切分&lt;/h2>
&lt;p>切「現在 ship vs 下輪 ship」用三個維度：&lt;/p>
&lt;h3 id="軸-1使用者可見性">軸 1：使用者可見性&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>高&lt;/strong>：使用者立刻能感受到差異（UI 改變、訊息精準、互動更順）&lt;/li>
&lt;li>&lt;strong>低&lt;/strong>：純內部結構（refactor、index 重建、protocol 升級）&lt;/li>
&lt;/ul>
&lt;p>可見性高 → 早 ship 拿價值；可見性低 → 早晚 ship 差別不大、可以等更多 confidence。&lt;/p>
&lt;h3 id="軸-2風險暴露面">軸 2：風險暴露面&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>低&lt;/strong>：純加法（新檔案、新欄位、新 endpoint）— 不影響既有 path&lt;/li>
&lt;li>&lt;strong>中&lt;/strong>：修改既有 code path 但有 fallback / 開關&lt;/li>
&lt;li>&lt;strong>高&lt;/strong>：替換、刪除、結構重組 — 沒退路或退路成本高&lt;/li>
&lt;/ul>
&lt;p>低風險 → 早 ship、出問題範圍小；高風險 → 等 confidence、配 staged rollout / feature flag。&lt;/p>
&lt;h3 id="軸-3驗證需求">軸 3：驗證需求&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>低&lt;/strong>：邏輯簡單、unit test 夠、可肉眼驗收&lt;/li>
&lt;li>&lt;strong>中&lt;/strong>：需要 E2E、多瀏覽器 / 多裝置驗證&lt;/li>
&lt;li>&lt;strong>高&lt;/strong>：需要長時觀測、production 流量壓測、A/B 比較&lt;/li>
&lt;/ul>
&lt;p>低驗證需求 → 早 ship；高驗證需求 → 等驗證流程跑完、不為趕時間跳過驗收。&lt;/p>
&lt;hr>
&lt;h2 id="切分矩陣">切分矩陣&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>可見性&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;th>驗證&lt;/th>
 &lt;th>建議&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>&lt;strong>立刻 ship&lt;/strong>（最高 ROI / 風險比）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>跑完 E2E 就 ship&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;td>配 feature flag、staged rollout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>順便 ship、合併進其他 PR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>&lt;strong>下輪&lt;/strong>（沒急、值得等驗證）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>看 batch 是否方便、不單獨 ship&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵 row：&lt;strong>「高可見 + 低風險 + 低驗證」就是先 ship 的甜蜜點&lt;/strong> — 例：UX hint、empty state 訊息、明顯的 UI 修正。&lt;/p>
&lt;hr>
&lt;h2 id="先-ship-dbc-下輪的典型範例">「先 ship D、B/C 下輪」的典型範例&lt;/h2>
&lt;p>來源：&lt;a href="../search-engine-matching-mode-mismatch/">#73 prefix-match 限制&lt;/a>&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>策略&lt;/th>
 &lt;th>軸 1 可見性&lt;/th>
 &lt;th>軸 2 風險&lt;/th>
 &lt;th>軸 3 驗證&lt;/th>
 &lt;th>排序&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>D（UX hint：「搜尋為前綴匹配」）&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>低（純加 UI 文字）&lt;/td>
 &lt;td>低（不影響既有功能）&lt;/td>
 &lt;td>&lt;strong>先 ship&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>C（client-side substring fallback）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中（多一條 path）&lt;/td>
 &lt;td>中（要驗證效能）&lt;/td>
 &lt;td>下輪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B（build-time pre-tokenize）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>高（改 build pipeline）&lt;/td>
 &lt;td>高（要驗證 index size、search ranking）&lt;/td>
 &lt;td>下輪&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>D 滿足「高可見 + 低風險 + 低驗證」、立刻 ship 解眼前混亂。B/C 解根因、但風險與驗證需求高、下輪做。&lt;strong>這個排序不是「重要程度」、是「ship 順序」&lt;/strong> — 重要程度 B/C &amp;gt; D、但 ship 順序 D &amp;gt; B &amp;gt; C。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>寫到「該 ship 哪些」時、預設<strong>分批</strong>：把 changes 沿三軸切 — <strong>使用者可見性高 + 風險低 + 驗證簡單</strong> 的先 ship、<strong>結構性 + 風險高 + 需驗證</strong> 的下輪。對抗「都做完才能 ship」的整體性衝動。</p>
<p>分批的真正價值：<strong>降低每次 review 的 cognitive load + 加速使用者拿到價值 + 讓回退單位更小</strong>。整批 ship 的代價是 review 變慢、bug 排查面變大、出問題回退要拖整批。</p>
<hr>
<h2 id="三軸切分">三軸切分</h2>
<p>切「現在 ship vs 下輪 ship」用三個維度：</p>
<h3 id="軸-1使用者可見性">軸 1：使用者可見性</h3>
<ul>
<li><strong>高</strong>：使用者立刻能感受到差異（UI 改變、訊息精準、互動更順）</li>
<li><strong>低</strong>：純內部結構（refactor、index 重建、protocol 升級）</li>
</ul>
<p>可見性高 → 早 ship 拿價值；可見性低 → 早晚 ship 差別不大、可以等更多 confidence。</p>
<h3 id="軸-2風險暴露面">軸 2：風險暴露面</h3>
<ul>
<li><strong>低</strong>：純加法（新檔案、新欄位、新 endpoint）— 不影響既有 path</li>
<li><strong>中</strong>：修改既有 code path 但有 fallback / 開關</li>
<li><strong>高</strong>：替換、刪除、結構重組 — 沒退路或退路成本高</li>
</ul>
<p>低風險 → 早 ship、出問題範圍小；高風險 → 等 confidence、配 staged rollout / feature flag。</p>
<h3 id="軸-3驗證需求">軸 3：驗證需求</h3>
<ul>
<li><strong>低</strong>：邏輯簡單、unit test 夠、可肉眼驗收</li>
<li><strong>中</strong>：需要 E2E、多瀏覽器 / 多裝置驗證</li>
<li><strong>高</strong>：需要長時觀測、production 流量壓測、A/B 比較</li>
</ul>
<p>低驗證需求 → 早 ship；高驗證需求 → 等驗證流程跑完、不為趕時間跳過驗收。</p>
<hr>
<h2 id="切分矩陣">切分矩陣</h2>
<table>
  <thead>
      <tr>
          <th>可見性</th>
          <th>風險</th>
          <th>驗證</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高</td>
          <td>低</td>
          <td>低</td>
          <td><strong>立刻 ship</strong>（最高 ROI / 風險比）</td>
      </tr>
      <tr>
          <td>高</td>
          <td>低</td>
          <td>中</td>
          <td>跑完 E2E 就 ship</td>
      </tr>
      <tr>
          <td>高</td>
          <td>高</td>
          <td>中-高</td>
          <td>配 feature flag、staged rollout</td>
      </tr>
      <tr>
          <td>低</td>
          <td>低</td>
          <td>低</td>
          <td>順便 ship、合併進其他 PR</td>
      </tr>
      <tr>
          <td>低</td>
          <td>高</td>
          <td>高</td>
          <td><strong>下輪</strong>（沒急、值得等驗證）</td>
      </tr>
      <tr>
          <td>低</td>
          <td>中</td>
          <td>中</td>
          <td>看 batch 是否方便、不單獨 ship</td>
      </tr>
  </tbody>
</table>
<p>關鍵 row：<strong>「高可見 + 低風險 + 低驗證」就是先 ship 的甜蜜點</strong> — 例：UX hint、empty state 訊息、明顯的 UI 修正。</p>
<hr>
<h2 id="先-ship-dbc-下輪的典型範例">「先 ship D、B/C 下輪」的典型範例</h2>
<p>來源：<a href="../search-engine-matching-mode-mismatch/">#73 prefix-match 限制</a></p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>軸 1 可見性</th>
          <th>軸 2 風險</th>
          <th>軸 3 驗證</th>
          <th>排序</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>D（UX hint：「搜尋為前綴匹配」）</td>
          <td>高</td>
          <td>低（純加 UI 文字）</td>
          <td>低（不影響既有功能）</td>
          <td><strong>先 ship</strong></td>
      </tr>
      <tr>
          <td>C（client-side substring fallback）</td>
          <td>中</td>
          <td>中（多一條 path）</td>
          <td>中（要驗證效能）</td>
          <td>下輪</td>
      </tr>
      <tr>
          <td>B（build-time pre-tokenize）</td>
          <td>中</td>
          <td>高（改 build pipeline）</td>
          <td>高（要驗證 index size、search ranking）</td>
          <td>下輪</td>
      </tr>
  </tbody>
</table>
<p>D 滿足「高可見 + 低風險 + 低驗證」、立刻 ship 解眼前混亂。B/C 解根因、但風險與驗證需求高、下輪做。<strong>這個排序不是「重要程度」、是「ship 順序」</strong> — 重要程度 B/C &gt; D、但 ship 順序 D &gt; B &gt; C。</p>
<hr>
<h2 id="為什麼全做完才-ship是反模式">為什麼「全做完才 ship」是反模式</h2>
<p>幾個常見藉口 + 為什麼站不住：</p>
<table>
  <thead>
      <tr>
          <th>藉口</th>
          <th>為什麼站不住</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「分批 ship 不完整」</td>
          <td>完整是工程師視角、使用者只看自己當下能不能用上</td>
      </tr>
      <tr>
          <td>「PR 越大越好 review」</td>
          <td>反、PR 越大 review 越粗、bug 越多漏</td>
      </tr>
      <tr>
          <td>「下輪我會做完」</td>
          <td>違反 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a> — 沒 trigger 會跳過</td>
      </tr>
      <tr>
          <td>「測試一起 ship 比較好驗」</td>
          <td>反、批次測試會放大 noise、各個獨立驗證更乾淨</td>
      </tr>
      <tr>
          <td>「regression 一起爆比較好排查」</td>
          <td>反、regression 範圍越大越難 bisect</td>
      </tr>
  </tbody>
</table>
<p>實際上「全做完才 ship」最常見的真實原因是：<strong>沒花時間想分批</strong>。預設分批就會自然分。</p>
<hr>
<h2 id="分批反模式">分批反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把高風險砍進「先 ship」 batch 為了趕 demo</td>
          <td>風險爆炸時所有先 ship 的內容跟著退</td>
          <td>用 feature flag、不要硬塞</td>
      </tr>
      <tr>
          <td>「下輪做 X」沒寫進系統</td>
          <td>X 變成 <a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a></td>
          <td>寫成 issue / TODO with deadline</td>
      </tr>
      <tr>
          <td>第一批漏掉 telemetry</td>
          <td>下輪沒資料判斷 X 該怎麼設計</td>
          <td>第一批就埋觀測</td>
      </tr>
      <tr>
          <td>分太細、每個 PR 都太小、整體 review 成本反而高</td>
          <td>分批本身有 overhead</td>
          <td>每批 ≥ 一個完整使用者 user-story 的價值</td>
      </tr>
      <tr>
          <td>第一批 ship 後就鬆懈、忘了下輪</td>
          <td>結構性陷阱</td>
          <td>把下輪寫進 calendar / sprint plan</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="何時該堅持一次完整-ship">何時該堅持「一次完整 ship」</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Feature 拆了不能用（atomic from user view）</td>
          <td>強制 atomic、用 feature flag 控制可見性</td>
      </tr>
      <tr>
          <td>Migration / Schema change</td>
          <td>半 ship 會破壞既有資料 / 流程一致性</td>
      </tr>
      <tr>
          <td>安全修補</td>
          <td>不能 leak 知道一半</td>
      </tr>
      <tr>
          <td>跨服務 protocol upgrade（client + server 必須對齊）</td>
          <td>半邊改另一半就破</td>
      </tr>
      <tr>
          <td>第一次設定 baseline</td>
          <td>沒 baseline 可比較、下輪改才有 reference</td>
      </tr>
  </tbody>
</table>
<p>四類共通：<strong>ship 一半比都不 ship 更壞</strong>。其他情境分批優先。</p>
<hr>
<h2 id="跟其他卡的關係">跟其他卡的關係</h2>
<table>
  <thead>
      <tr>
          <th>卡</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>分批 ship 對應「Ship 前 / Ship 後」分散 — 每批各自走完四 checkpoint</td>
      </tr>
      <tr>
          <td><a href="../main-strategy-plus-supplementary/">#75 主策略 + 補強</a></td>
          <td>補強策略通常先 ship、主策略下輪 — 兩卡互補</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>「下輪做」需要結構性 trigger（issue + deadline）、不靠紀律</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>每批的範圍從窄起、有證據再擴張</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>本卡是 #79「批次邊界」維度的展開 — 一次 vs 分批</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PR diff &gt; 800 行、含多個 feature</td>
          <td>拆批、各自走 review</td>
      </tr>
      <tr>
          <td>「等 X 做完一起 ship」</td>
          <td>用三軸檢查 X 是否該獨立 ship</td>
      </tr>
      <tr>
          <td>Feature flag 名稱長期堆積、沒清掉</td>
          <td>「下輪清掉」沒 trigger、補 <a href="../external-trigger-for-high-roi-work/">#72 L3-L5 對策</a></td>
      </tr>
      <tr>
          <td>「這次先這樣、下次再優化」每次都不發生</td>
          <td>下輪沒 trigger、把它寫進系統</td>
      </tr>
      <tr>
          <td>第一批 ship 後 production 出問題、回退範圍大</td>
          <td>第一批塞太多、檢查為什麼沒分更細</td>
      </tr>
      <tr>
          <td>使用者抱怨「等很久才有 X」</td>
          <td>可能 X 早就可分批 ship、檢查阻塞點</td>
      </tr>
      <tr>
          <td>推薦「等 B/C 都做完再 ship」</td>
          <td>違反三軸、應該 D 先 ship</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：「ship 順序 ≠ 重要程度」。使用者可見性高 + 風險低 + 驗證需求低 = 先 ship 甜蜜點、即使在重要程度上不是 top。等所有結構性修法都做完才 ship、是把重要程度誤當成 ship 順序的常見錯誤。</p>
]]></content:encoded></item><item><title>卡片系統的迭代浮現：原子卡 → meta-卡 → reference 三層展開</title><link>https://tarrragon.github.io/blog/report/cards-as-living-system-iteration/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/cards-as-living-system-iteration/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>知識卡片系統的成型不是「想清楚再寫」、是&lt;strong>多輪迭代浮現&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">原始對話素材
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ 識別重複結構
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">原子卡（每張一個小現象）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓ 串連、識別共同骨架
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">meta-卡（抽上層原則）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> ↓ 沉澱成可重複使用的 protocol
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">reference（可直接套用的 checklist + 模板）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> ↓ L3 觸發機制
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">SKILL（自動觸發 reference）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每層都解上一層的限制、不是替代。&lt;strong>原子卡保留具體 case 的細節&lt;/strong>（被反例反駁時可保留）、&lt;strong>meta-卡提供跨情境的判讀框架&lt;/strong>（避免每次重新推理）、&lt;strong>reference 沉澱成可直接套用的步驟&lt;/strong>（消除「知道但忘記用」的鴻溝）。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼一次寫不完">為什麼一次寫不完&lt;/h2>
&lt;p>第一次接觸現象時、看到的是&lt;strong>具體 case 的表面&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>看到「使用者說『我再想想』」 → 先寫成「[#77] 延後是合法選項」&lt;/li>
&lt;li>看到「使用者說『1+2』」 → 先寫成「[#78] 反省題複選」&lt;/li>
&lt;li>看到「使用者反駁推薦」 → 先寫成「[#74] 決策呈現格式」&lt;/li>
&lt;/ul>
&lt;p>每張原子卡解 1 個情境、自包含可讀。但&lt;strong>串連在一起時才浮現的結構&lt;/strong>（例：「五個獨立維度」）需要看到 ≥ 3-5 張原子卡之後才看得出。&lt;strong>第一次寫不出來、不是因為沒想清楚、是因為原料不夠&lt;/strong>。&lt;/p>
&lt;p>催熟原子卡之前先寫 meta-卡 = 從少數 case 過度推論、產生 over-fit 結構、後續發現新 case 不符就要重寫。&lt;/p>
&lt;hr>
&lt;h2 id="三層的職責分工">三層的職責分工&lt;/h2>
&lt;h3 id="layer-1原子卡">Layer 1：原子卡&lt;/h3>
&lt;p>&lt;strong>範圍&lt;/strong>：單一現象 / 單一錯誤 / 單一情境。&lt;/p>
&lt;p>&lt;strong>特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>從具體事件浮現（事後檢討）&lt;/li>
&lt;li>自包含、不依賴其他卡也能讀&lt;/li>
&lt;li>含「反模式 / 修法 / 何時不適用」三段&lt;/li>
&lt;li>給未來自己看：「啊我再次遇到這個」&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項&lt;/a> 是從一次具體對話中「使用者說『不用現在決策』、agent 加壓」浮現。&lt;/p>
&lt;h3 id="layer-2meta-卡">Layer 2：Meta-卡&lt;/h3>
&lt;p>&lt;strong>範圍&lt;/strong>：N 張原子卡的共同骨架。&lt;/p>
&lt;p>&lt;strong>特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>不是新原則、是把已存在的原則上抽&lt;/li>
&lt;li>通常出現在「寫 N 張原子卡之後、發現他們其實同一件事」&lt;/li>
&lt;li>提供跨情境判讀（&amp;ldquo;這個情境屬於哪一維度?&amp;quot;）&lt;/li>
&lt;li>給「已有 mental model 的讀者」加深、不取代原子卡&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度&lt;/a> 是寫完 [#74-#78] 五張原子卡後、發現他們各對應一個獨立維度。沒寫 #79 之前 #74-#78 是五張平行卡、寫完 #79 後形成有結構的網。&lt;/p>
&lt;h3 id="layer-3reference">Layer 3：Reference&lt;/h3>
&lt;p>&lt;strong>範圍&lt;/strong>：把 N 張卡的判讀流程沉澱成可直接套用的 step-by-step。&lt;/p>
&lt;p>&lt;strong>特徵&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>不是教學、是 lookup table + checklist&lt;/li>
&lt;li>在實作中被翻開、不是讀爽的&lt;/li>
&lt;li>結尾有 self-check 讓使用者驗證自己沒漏&lt;/li>
&lt;li>跟一張具體任務 / 觸發情境對應&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>例&lt;/strong>：&lt;code>references/decision-dialogue.md&lt;/code>（在 SKILL 內）— 把 #74-#79 翻譯成「五步判讀 + 完整模板 + self-check」、agent 寫 decision 之前看一遍就夠了。&lt;/p>
&lt;hr>
&lt;h2 id="多層迭代的訊號什麼時候該往上抽">多層迭代的訊號：什麼時候該往上抽？&lt;/h2>
&lt;h3 id="訊號-1寫第-n-張卡時發現大段內容跟前一張重複">訊號 1：寫第 N 張卡時、發現大段內容跟前一張重複&lt;/h3>
&lt;p>→ 兩張卡共用某個結構、抽出 meta-卡。例：寫 [#78] 反省題複選時、引用 [#74] 推薦格式 = 暗示有上層共骨。&lt;/p>
&lt;h3 id="訊號-2跨卡-cross-link-變密單張卡的跟其他卡的關係段持續長">訊號 2：跨卡 cross-link 變密、單張卡的「跟其他卡的關係」段持續長&lt;/h3>
&lt;p>→ 知識網密度足夠、可抽 meta-卡作為樞紐。&lt;/p>
&lt;h3 id="訊號-3實作中要回查多張卡才能完整-apply">訊號 3：實作中要回查多張卡才能完整 apply&lt;/h3>
&lt;p>→ 沉澱成 reference、減少回查成本。&lt;/p>
&lt;h3 id="訊號-4我之前是不是寫過類似的第-3-次出現">訊號 4：「我之前是不是寫過類似的」第 3 次出現&lt;/h3>
&lt;p>→ 不是「沒寫過」、是 meta-結構模糊、無法用既有卡 frame 新情境。需要 meta-卡。&lt;/p>
&lt;hr>
&lt;h2 id="反模式跳層的代價">反模式：跳層的代價&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>反模式&lt;/th>
 &lt;th>為什麼不好&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>直接從對話寫 meta-卡（沒原子卡支撐）&lt;/td>
 &lt;td>over-fit 少數 case、新 case 不符就要重寫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>只寫 reference 不寫卡片&lt;/td>
 &lt;td>reference 是「怎麼做」、原子卡是「為什麼」、缺少 why 後續難 maintain&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>卡片寫完不抽 meta&lt;/td>
 &lt;td>知識散落、跨情境無法判讀、實作中要回查多張&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Meta-卡寫太早（寫第 1-2 張就抽）&lt;/td>
 &lt;td>沒足夠 N 看出共骨、結構強加&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一張卡裡塞多個現象&lt;/td>
 &lt;td>卡片該原子、混合會干擾 cross-link&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Reference 沒對應觸發情境&lt;/td>
 &lt;td>寫了沒人看、變另一份未來才會被翻的文件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>卡片寫完不回頭 cross-link&lt;/td>
 &lt;td>知識網不形成、留下孤兒卡&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="觀察多層迭代不是線性是-spiral">觀察：多層迭代不是線性、是 spiral&lt;/h2>
&lt;p>實際上的迭代不是「Layer 1 全寫完才寫 Layer 2」、而是：&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>知識卡片系統的成型不是「想清楚再寫」、是<strong>多輪迭代浮現</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">原始對話素材
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ 識別重複結構
</span></span><span class="line"><span class="ln">3</span><span class="cl">原子卡（每張一個小現象）
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ 串連、識別共同骨架
</span></span><span class="line"><span class="ln">5</span><span class="cl">meta-卡（抽上層原則）
</span></span><span class="line"><span class="ln">6</span><span class="cl">   ↓ 沉澱成可重複使用的 protocol
</span></span><span class="line"><span class="ln">7</span><span class="cl">reference（可直接套用的 checklist + 模板）
</span></span><span class="line"><span class="ln">8</span><span class="cl">   ↓ L3 觸發機制
</span></span><span class="line"><span class="ln">9</span><span class="cl">SKILL（自動觸發 reference）</span></span></code></pre></div><p>每層都解上一層的限制、不是替代。<strong>原子卡保留具體 case 的細節</strong>（被反例反駁時可保留）、<strong>meta-卡提供跨情境的判讀框架</strong>（避免每次重新推理）、<strong>reference 沉澱成可直接套用的步驟</strong>（消除「知道但忘記用」的鴻溝）。</p>
<hr>
<h2 id="為什麼一次寫不完">為什麼一次寫不完</h2>
<p>第一次接觸現象時、看到的是<strong>具體 case 的表面</strong>：</p>
<ul>
<li>看到「使用者說『我再想想』」 → 先寫成「[#77] 延後是合法選項」</li>
<li>看到「使用者說『1+2』」 → 先寫成「[#78] 反省題複選」</li>
<li>看到「使用者反駁推薦」 → 先寫成「[#74] 決策呈現格式」</li>
</ul>
<p>每張原子卡解 1 個情境、自包含可讀。但<strong>串連在一起時才浮現的結構</strong>（例：「五個獨立維度」）需要看到 ≥ 3-5 張原子卡之後才看得出。<strong>第一次寫不出來、不是因為沒想清楚、是因為原料不夠</strong>。</p>
<p>催熟原子卡之前先寫 meta-卡 = 從少數 case 過度推論、產生 over-fit 結構、後續發現新 case 不符就要重寫。</p>
<hr>
<h2 id="三層的職責分工">三層的職責分工</h2>
<h3 id="layer-1原子卡">Layer 1：原子卡</h3>
<p><strong>範圍</strong>：單一現象 / 單一錯誤 / 單一情境。</p>
<p><strong>特徵</strong>：</p>
<ul>
<li>從具體事件浮現（事後檢討）</li>
<li>自包含、不依賴其他卡也能讀</li>
<li>含「反模式 / 修法 / 何時不適用」三段</li>
<li>給未來自己看：「啊我再次遇到這個」</li>
</ul>
<p><strong>例</strong>：<a href="../decide-later-as-valid-option/">#77 「現在不決定」是合法選項</a> 是從一次具體對話中「使用者說『不用現在決策』、agent 加壓」浮現。</p>
<h3 id="layer-2meta-卡">Layer 2：Meta-卡</h3>
<p><strong>範圍</strong>：N 張原子卡的共同骨架。</p>
<p><strong>特徵</strong>：</p>
<ul>
<li>不是新原則、是把已存在的原則上抽</li>
<li>通常出現在「寫 N 張原子卡之後、發現他們其實同一件事」</li>
<li>提供跨情境判讀（&ldquo;這個情境屬於哪一維度?&quot;）</li>
<li>給「已有 mental model 的讀者」加深、不取代原子卡</li>
</ul>
<p><strong>例</strong>：<a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度</a> 是寫完 [#74-#78] 五張原子卡後、發現他們各對應一個獨立維度。沒寫 #79 之前 #74-#78 是五張平行卡、寫完 #79 後形成有結構的網。</p>
<h3 id="layer-3reference">Layer 3：Reference</h3>
<p><strong>範圍</strong>：把 N 張卡的判讀流程沉澱成可直接套用的 step-by-step。</p>
<p><strong>特徵</strong>：</p>
<ul>
<li>不是教學、是 lookup table + checklist</li>
<li>在實作中被翻開、不是讀爽的</li>
<li>結尾有 self-check 讓使用者驗證自己沒漏</li>
<li>跟一張具體任務 / 觸發情境對應</li>
</ul>
<p><strong>例</strong>：<code>references/decision-dialogue.md</code>（在 SKILL 內）— 把 #74-#79 翻譯成「五步判讀 + 完整模板 + self-check」、agent 寫 decision 之前看一遍就夠了。</p>
<hr>
<h2 id="多層迭代的訊號什麼時候該往上抽">多層迭代的訊號：什麼時候該往上抽？</h2>
<h3 id="訊號-1寫第-n-張卡時發現大段內容跟前一張重複">訊號 1：寫第 N 張卡時、發現大段內容跟前一張重複</h3>
<p>→ 兩張卡共用某個結構、抽出 meta-卡。例：寫 [#78] 反省題複選時、引用 [#74] 推薦格式 = 暗示有上層共骨。</p>
<h3 id="訊號-2跨卡-cross-link-變密單張卡的跟其他卡的關係段持續長">訊號 2：跨卡 cross-link 變密、單張卡的「跟其他卡的關係」段持續長</h3>
<p>→ 知識網密度足夠、可抽 meta-卡作為樞紐。</p>
<h3 id="訊號-3實作中要回查多張卡才能完整-apply">訊號 3：實作中要回查多張卡才能完整 apply</h3>
<p>→ 沉澱成 reference、減少回查成本。</p>
<h3 id="訊號-4我之前是不是寫過類似的第-3-次出現">訊號 4：「我之前是不是寫過類似的」第 3 次出現</h3>
<p>→ 不是「沒寫過」、是 meta-結構模糊、無法用既有卡 frame 新情境。需要 meta-卡。</p>
<hr>
<h2 id="反模式跳層的代價">反模式：跳層的代價</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>直接從對話寫 meta-卡（沒原子卡支撐）</td>
          <td>over-fit 少數 case、新 case 不符就要重寫</td>
      </tr>
      <tr>
          <td>只寫 reference 不寫卡片</td>
          <td>reference 是「怎麼做」、原子卡是「為什麼」、缺少 why 後續難 maintain</td>
      </tr>
      <tr>
          <td>卡片寫完不抽 meta</td>
          <td>知識散落、跨情境無法判讀、實作中要回查多張</td>
      </tr>
      <tr>
          <td>Meta-卡寫太早（寫第 1-2 張就抽）</td>
          <td>沒足夠 N 看出共骨、結構強加</td>
      </tr>
      <tr>
          <td>一張卡裡塞多個現象</td>
          <td>卡片該原子、混合會干擾 cross-link</td>
      </tr>
      <tr>
          <td>Reference 沒對應觸發情境</td>
          <td>寫了沒人看、變另一份未來才會被翻的文件</td>
      </tr>
      <tr>
          <td>卡片寫完不回頭 cross-link</td>
          <td>知識網不形成、留下孤兒卡</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="觀察多層迭代不是線性是-spiral">觀察：多層迭代不是線性、是 spiral</h2>
<p>實際上的迭代不是「Layer 1 全寫完才寫 Layer 2」、而是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">寫 #74 → 寫 #75 → (浮現 meta) → 草稿 #79 →
</span></span><span class="line"><span class="ln">2</span><span class="cl">寫 #76 → (補 #79) → 寫 #77 → (補 #79) →
</span></span><span class="line"><span class="ln">3</span><span class="cl">寫 #78 → 完成 #79 → 寫 reference → SKILL 整合</span></span></code></pre></div><p>每次新卡可能反過來修改 meta-卡、reference 也可能反過來指出原子卡缺角。<strong>Spiral 結構接受迭代修正、線性結構假裝一次寫對</strong>。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>寫 meta-卡的訊號：第 2 次看到類似結構、抽出來</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>先寫原子卡、有證據再抽 meta、跟「先窄後寬」同構</td>
      </tr>
      <tr>
          <td><a href="../single-source-of-truth/">#44 SSOT</a></td>
          <td>meta-卡是上層 SSOT、原子卡保留 case-specific 細節、各層分工</td>
      </tr>
      <tr>
          <td><a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度</a></td>
          <td>「直接寫 meta」容易但會 over-fit、迭代浮現難寫但對齊真實結構</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>「回頭抽 meta + 寫 reference」是高 ROI 但無觸發、需要協議 / pair / 對話結構驅動</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五個維度</a></td>
          <td>本卡的 spiral 過程剛好就是 #79 浮現的實例 — meta-卡 + reference 都是後寫</td>
      </tr>
      <tr>
          <td><a href="../literal-interception-vs-behavioral-refinement/">#82 字面攔截 vs 行為精煉</a></td>
          <td>spiral 是 multi-pass refinement 的具體實現 — 卡片內容對不對、抽 meta 抽得對不對都是行為錯誤、靠 spiral 收斂、不靠 hook 攔截</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="套用到本系統的具體-case">套用到本系統的具體 case</h2>
<p><code>content/report/</code> 的 80+ 卡片成型路徑：</p>
<ol>
<li><strong>第 1-2 輪</strong>（#1-#30）：純事後檢討、單張原子卡、互不串連</li>
<li><strong>第 3 輪</strong>（#31-#45）：開始抽 pattern 卡、識別重複結構</li>
<li><strong>第 4 輪</strong>（#42-#45 + #67-#72）：抽出第一批 meta-卡</li>
<li><strong>第 5 輪</strong>（#55-#73）：寫 #59 五策略時發現 meta-卡需求、回補 #67-#73</li>
<li><strong>第 6 輪</strong>（#74-#80）：dialogue 中浮現決策協議、寫原子卡 + meta + reference</li>
<li><strong>下一輪</strong>：可能會在 #80 上面浮現另一層 meta（process 反思的 meta）</li>
</ol>
<p>每輪都不是「一次寫完」、是 spiral 中的一個 lap。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第 N 張卡、結構大段重複前卡</td>
          <td>抽 meta-卡</td>
      </tr>
      <tr>
          <td>卡片網的 cross-link 變密</td>
          <td>加 meta-卡作為樞紐</td>
      </tr>
      <tr>
          <td>實作中要翻 ≥ 3 張卡</td>
          <td>沉澱 reference</td>
      </tr>
      <tr>
          <td>「之前好像寫過類似的」第 3 次</td>
          <td>缺 meta-frame、補上</td>
      </tr>
      <tr>
          <td>Reference 寫完沒人翻</td>
          <td>沒接到觸發情境、補 SKILL trigger route</td>
      </tr>
      <tr>
          <td>Meta-卡寫太早、後續新 case 一直破壞</td>
          <td>退回原子卡層、累積到 ≥ 3-5 張再抽</td>
      </tr>
      <tr>
          <td>原子卡卡得很細、單張看完不知道幹嘛</td>
          <td>缺 meta-上下文、補 meta-卡或 reference</td>
      </tr>
      <tr>
          <td>Cross-link 偏單向（只引用、沒被引用）</td>
          <td>孤兒卡、反向 link 補回</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：知識卡片系統不是寫一次的文件、是長期 spiral 迭代的 living system。<strong>接受「第一次寫不對、會迭代」這個前提</strong>、就會在每次接觸新現象時先寫原子、累積到一定 N 後抽 meta、最後沉澱 reference。<strong>反過來的「想清楚再寫」是模仿線性開發、跟知識浮現的真實結構不對齊</strong>。</p>
]]></content:encoded></item><item><title>字面攔截 vs 行為精煉：驗證手段跟錯誤層次的對齊</title><link>https://tarrragon.github.io/blog/report/literal-interception-vs-behavioral-refinement/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/literal-interception-vs-behavioral-refinement/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>驗證手段（hook / lint / CI / review / spiral / test / production observation）有不同的「錯誤偵測粒度」、必須跟&lt;strong>錯誤的層次&lt;/strong>對齊：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>錯誤層次&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;th>適合手段&lt;/th>
 &lt;th>不適合手段&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>字面&lt;/td>
 &lt;td>typo、缺 field、syntax 錯、檔案沒 frontmatter&lt;/td>
 &lt;td>hook、lint、type checker、schema validation&lt;/td>
 &lt;td>multi-pass review（過殺）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>行為&lt;/td>
 &lt;td>推薦騎牆、yes/no collapse、思考偏差、judgment 錯位&lt;/td>
 &lt;td>multi-pass spiral、review、dogfood&lt;/td>
 &lt;td>hook（catch 不到、假裝有保護）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「攔截」這個動作預設&lt;strong>已經知道錯誤的形狀&lt;/strong>（hook 寫死規則 = 已知錯誤）。&lt;strong>真正會出錯的是「不知道形狀」的錯誤&lt;/strong> — 那需要多輪 review / spiral 收斂、不是即時攔截。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-hook-對行為錯誤無能為力">為什麼 hook 對行為錯誤無能為力&lt;/h2>
&lt;p>Hook / lint / type checker 的本質是 &lt;strong>字串匹配 / structural check&lt;/strong> — 看得到形狀、看不到意圖。所以：&lt;/p>
&lt;ul>
&lt;li>抓得到「commit message 沒含 issue 號」 — 字面 pattern&lt;/li>
&lt;li>抓得到「test file 沒對應 source file」 — 結構檢查&lt;/li>
&lt;li>抓得到「YAML frontmatter 缺欄位」 — schema check&lt;/li>
&lt;li>抓不到「這個推薦不夠明確、騎牆」 — 需要理解語意&lt;/li>
&lt;li>抓不到「決策 collapse 到 yes/no、漏五維」 — 需要判斷意圖&lt;/li>
&lt;li>抓不到「思考路徑跳過 RED phase」 — 需要追溯 reasoning&lt;/li>
&lt;li>抓不到「過度疊加策略、超過必要」 — 需要 judgment&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Hook 試圖用字串規則模擬語意檢查 = 規則永遠 over-fit 或 under-fit&lt;/strong>：寫太嚴 → 大量 false positive 把好的也擋掉、寫太鬆 → 行為錯誤照樣通過。&lt;/p>
&lt;hr>
&lt;h2 id="反模式用-hook-蓋行為錯誤的代價">反模式：用 hook 蓋行為錯誤的代價&lt;/h2>
&lt;h3 id="false-confidence-比沒保護更危險">False confidence 比沒保護更危險&lt;/h3>
&lt;p>寫了 hook 之後、心理上會覺得「有保護」。實際上 hook 只擋字面、行為錯誤照常發生 — 但作者不再警覺、因為「CI 通過了應該沒事」。&lt;/p>
&lt;p>對比沒 hook 的情境：作者知道沒保護、會主動多看一次。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>警覺度&lt;/th>
 &lt;th>實際漏接率&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>沒 hook&lt;/td>
 &lt;td>高（知道沒保護）&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hook 抓不到的範圍誤以為有保護&lt;/td>
 &lt;td>低（誤以為有）&lt;/td>
 &lt;td>&lt;strong>高&lt;/strong>（行為錯誤通過）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hook 真的夠（純字面領域）&lt;/td>
 &lt;td>適中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>第二行是最危險的組合&lt;/strong> — 加 hook 卻不知道 hook 範圍、會比沒 hook 更糟。&lt;/p>
&lt;h3 id="規則膨脹嘗試再寫一條-hook永遠補不完">規則膨脹：嘗試「再寫一條 hook」永遠補不完&lt;/h3>
&lt;p>每次行為錯誤通過、直覺反應是「再加一條 hook 規則」。但行為錯誤的形狀是無限的、規則永遠補不完。最終結果：&lt;/p>
&lt;ul>
&lt;li>規則越來越多、越來越複雜&lt;/li>
&lt;li>維護成本爆炸&lt;/li>
&lt;li>仍然漏接行為錯誤&lt;/li>
&lt;li>還產生越來越多 false positive 把好的擋掉&lt;/li>
&lt;/ul>
&lt;p>→ 規則膨脹是「用錯工具」的訊號、不是「規則寫得不夠細」的訊號。&lt;/p>
&lt;hr>
&lt;h2 id="多輪精煉的設計spiral-取代攔截">多輪精煉的設計：spiral 取代攔截&lt;/h2>
&lt;p>行為錯誤的正確驗證手段是 &lt;strong>multi-pass spiral&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">第 1 輪：先做、看結果
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓ 發現 N 個問題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">第 2 輪：依結果調整 / 補強
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓ 發現 N-k 個問題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">第 3 輪：dogfood / 實際使用 / 反向自查
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> ↓ 收斂
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">（沒新問題 → 結束、有新問題 → 繼續迭代）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵設計：&lt;strong>不是「攔截錯誤」、是「設計每輪能 catch 不同層的錯誤」&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p>驗證手段（hook / lint / CI / review / spiral / test / production observation）有不同的「錯誤偵測粒度」、必須跟<strong>錯誤的層次</strong>對齊：</p>
<table>
  <thead>
      <tr>
          <th>錯誤層次</th>
          <th>例子</th>
          <th>適合手段</th>
          <th>不適合手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>字面</td>
          <td>typo、缺 field、syntax 錯、檔案沒 frontmatter</td>
          <td>hook、lint、type checker、schema validation</td>
          <td>multi-pass review（過殺）</td>
      </tr>
      <tr>
          <td>行為</td>
          <td>推薦騎牆、yes/no collapse、思考偏差、judgment 錯位</td>
          <td>multi-pass spiral、review、dogfood</td>
          <td>hook（catch 不到、假裝有保護）</td>
      </tr>
  </tbody>
</table>
<p>「攔截」這個動作預設<strong>已經知道錯誤的形狀</strong>（hook 寫死規則 = 已知錯誤）。<strong>真正會出錯的是「不知道形狀」的錯誤</strong> — 那需要多輪 review / spiral 收斂、不是即時攔截。</p>
<hr>
<h2 id="為什麼-hook-對行為錯誤無能為力">為什麼 hook 對行為錯誤無能為力</h2>
<p>Hook / lint / type checker 的本質是 <strong>字串匹配 / structural check</strong> — 看得到形狀、看不到意圖。所以：</p>
<ul>
<li>抓得到「commit message 沒含 issue 號」 — 字面 pattern</li>
<li>抓得到「test file 沒對應 source file」 — 結構檢查</li>
<li>抓得到「YAML frontmatter 缺欄位」 — schema check</li>
<li>抓不到「這個推薦不夠明確、騎牆」 — 需要理解語意</li>
<li>抓不到「決策 collapse 到 yes/no、漏五維」 — 需要判斷意圖</li>
<li>抓不到「思考路徑跳過 RED phase」 — 需要追溯 reasoning</li>
<li>抓不到「過度疊加策略、超過必要」 — 需要 judgment</li>
</ul>
<p><strong>Hook 試圖用字串規則模擬語意檢查 = 規則永遠 over-fit 或 under-fit</strong>：寫太嚴 → 大量 false positive 把好的也擋掉、寫太鬆 → 行為錯誤照樣通過。</p>
<hr>
<h2 id="反模式用-hook-蓋行為錯誤的代價">反模式：用 hook 蓋行為錯誤的代價</h2>
<h3 id="false-confidence-比沒保護更危險">False confidence 比沒保護更危險</h3>
<p>寫了 hook 之後、心理上會覺得「有保護」。實際上 hook 只擋字面、行為錯誤照常發生 — 但作者不再警覺、因為「CI 通過了應該沒事」。</p>
<p>對比沒 hook 的情境：作者知道沒保護、會主動多看一次。</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>警覺度</th>
          <th>實際漏接率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>沒 hook</td>
          <td>高（知道沒保護）</td>
          <td>中</td>
      </tr>
      <tr>
          <td>Hook 抓不到的範圍誤以為有保護</td>
          <td>低（誤以為有）</td>
          <td><strong>高</strong>（行為錯誤通過）</td>
      </tr>
      <tr>
          <td>Hook 真的夠（純字面領域）</td>
          <td>適中</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p><strong>第二行是最危險的組合</strong> — 加 hook 卻不知道 hook 範圍、會比沒 hook 更糟。</p>
<h3 id="規則膨脹嘗試再寫一條-hook永遠補不完">規則膨脹：嘗試「再寫一條 hook」永遠補不完</h3>
<p>每次行為錯誤通過、直覺反應是「再加一條 hook 規則」。但行為錯誤的形狀是無限的、規則永遠補不完。最終結果：</p>
<ul>
<li>規則越來越多、越來越複雜</li>
<li>維護成本爆炸</li>
<li>仍然漏接行為錯誤</li>
<li>還產生越來越多 false positive 把好的擋掉</li>
</ul>
<p>→ 規則膨脹是「用錯工具」的訊號、不是「規則寫得不夠細」的訊號。</p>
<hr>
<h2 id="多輪精煉的設計spiral-取代攔截">多輪精煉的設計：spiral 取代攔截</h2>
<p>行為錯誤的正確驗證手段是 <strong>multi-pass spiral</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">第 1 輪：先做、看結果
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓ 發現 N 個問題
</span></span><span class="line"><span class="ln">3</span><span class="cl">第 2 輪：依結果調整 / 補強
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓ 發現 N-k 個問題
</span></span><span class="line"><span class="ln">5</span><span class="cl">第 3 輪：dogfood / 實際使用 / 反向自查
</span></span><span class="line"><span class="ln">6</span><span class="cl">   ↓ 收斂
</span></span><span class="line"><span class="ln">7</span><span class="cl">（沒新問題 → 結束、有新問題 → 繼續迭代）</span></span></code></pre></div><p>關鍵設計：<strong>不是「攔截錯誤」、是「設計每輪能 catch 不同層的錯誤」</strong>。</p>
<h3 id="各輪的職責分工">各輪的職責分工</h3>
<table>
  <thead>
      <tr>
          <th>輪次</th>
          <th>適合 catch 什麼</th>
          <th>怎麼設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>第 1 輪：實作</td>
          <td>純執行、預期會有錯</td>
          <td>不要追求 perfect、跑起來看結果</td>
      </tr>
      <tr>
          <td>第 2 輪：自查 / 對比需求</td>
          <td>邏輯偏差、漏 case</td>
          <td>對比原始需求、列 Checkpoint 1（<a href="../verification-timeline-checkpoints/">#68</a>）</td>
      </tr>
      <tr>
          <td>第 3 輪：dogfood / production</td>
          <td>實際使用才浮現的問題</td>
          <td>真實 user / 真實流量、看回饋</td>
      </tr>
      <tr>
          <td>第 N 輪：反向自查</td>
          <td>上幾輪沒看到的盲點</td>
          <td>改換 frame（例如「假裝是另一個人 review」）</td>
      </tr>
  </tbody>
</table>
<p>每輪解上一輪沒看到的問題、不是重複同一檢查。</p>
<h3 id="不同輪適合不同的不對齊">不同輪適合不同的「不對齊」</h3>
<ul>
<li>第 1 輪 vs 需求 → 看「做出來的跟要的對不對齊」</li>
<li>第 2 輪 vs 邊界 case → 看「漏哪些情境」</li>
<li>第 3 輪 vs 真實使用 → 看「用起來感覺對不對」</li>
<li>第 N 輪 vs 上層原則 → 看「有沒有違反某個 meta-原則」</li>
</ul>
<p>每輪有不同的角度、新角度才能 catch 上一輪 miss 的東西。</p>
<hr>
<h2 id="何時-hook-真的足夠">何時 hook 真的足夠</h2>
<p>某些情境純字面就夠、加 hook 是對的：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼 hook 夠</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema validation（API、DB、config）</td>
          <td>結構是 spec、字面對 = 行為對</td>
      </tr>
      <tr>
          <td>已知的 anti-pattern 字串（<code>TODO:</code>、<code>FIXME:</code>、<code>console.log</code>）</td>
          <td>字面就是 evidence</td>
      </tr>
      <tr>
          <td>格式統一（換行、縮排、import 順序）</td>
          <td>純美化、沒語意</td>
      </tr>
      <tr>
          <td>不可破壞的 invariant（commit 訊息含 issue 號、test 名格式）</td>
          <td>結構即正確</td>
      </tr>
      <tr>
          <td>安全 critical 的 surface check（沒 secret 在 code、license header 在）</td>
          <td>漏掉成本極高、字面檢查 ROI 高</td>
      </tr>
  </tbody>
</table>
<p>五類共通：<strong>錯誤形狀完全字面、且漏掉成本高 / 字面就是 evidence</strong>。其他情境 hook 都會在某個時點走到 ceiling。</p>
<hr>
<h2 id="識別-ceiling什麼時候該換手段">識別 ceiling：什麼時候該換手段</h2>
<p>ceiling 訊號：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換的手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「這個 lint 規則寫不出來、太多例外」</td>
          <td>改 review checklist、不寫 lint</td>
      </tr>
      <tr>
          <td>「hook pass 但 production 還是出錯」</td>
          <td>hook 已到 ceiling、補 multi-pass review</td>
      </tr>
      <tr>
          <td>「規則第 N 次補例外」</td>
          <td>規則膨脹、退回 review</td>
      </tr>
      <tr>
          <td>「false positive 比 true positive 多」</td>
          <td>hook 過殺、放寬 + 補 review</td>
      </tr>
      <tr>
          <td>「需要 understand intent 才能判斷」</td>
          <td>純字面不夠、要 LLM / human review</td>
      </tr>
      <tr>
          <td>「加了 hook 後 review 變草率」</td>
          <td>False confidence 在發生、警覺度降低</td>
      </tr>
  </tbody>
</table>
<p>看到任一訊號、不是「再寫一條 hook」、是<strong>接受 hook 對這個錯誤層次無能為力、改設計 multi-pass review</strong>。</p>
<hr>
<h2 id="跟其他抽象層原則的關係">跟其他抽象層原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a></td>
          <td>第 2 輪是 multi-pass 的最小單位、跟本卡的「多輪設計」同骨</td>
      </tr>
      <tr>
          <td><a href="../verification-timeline-checkpoints/">#68 驗收的時間軸</a></td>
          <td>#68 的四個 checkpoint = 多輪 review 的時間軸實現</td>
      </tr>
      <tr>
          <td><a href="../test-first-red-before-green/">#69 Test-First：RED before GREEN</a></td>
          <td>RED phase 是「testing the test」的多輪設計 — 純 hook 看不到</td>
      </tr>
      <tr>
          <td><a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無觸發</a></td>
          <td>#72 提倡 L3-L5 結構性對策、本卡是 ceiling — L5 hook 抓不到行為錯誤、需要 L4 review / pair</td>
      </tr>
      <tr>
          <td><a href="../cards-as-living-system-iteration/">#81 卡片系統的迭代浮現</a></td>
          <td>spiral 浮現本身就是 multi-pass 的具體 case — 不靠單次「寫對」</td>
      </tr>
      <tr>
          <td><a href="../decision-dialogue-dimensions/">#79 決策對話的五維度</a></td>
          <td>「五維 collapse」是行為錯誤、hook 抓不到、要靠 reference dogfood + multi-pass review</td>
      </tr>
      <tr>
          <td><a href="../writing-multi-pass-review/">#83 Writing 的 multi-pass review</a></td>
          <td>本卡在「寫」這個動作的具體實例 — review 是 multi-pass、不是 hook</td>
      </tr>
      <tr>
          <td><a href="../naming-as-iterated-artifact/">#84 Naming 是 iterated artifact</a></td>
          <td>本卡在「命名」這個動作的具體實例 — 命名 lint 只擋字面、grep / 一致性 / impl 洩漏靠 review</td>
      </tr>
      <tr>
          <td><a href="../methodology-multi-pass-embedding/">#85 Methodology 的 multi-pass 該 embed 在 pillar</a></td>
          <td>本卡在「方法論設計本身」這一層的展現 — multi-pass 升 pillar 才結構性執行</td>
      </tr>
      <tr>
          <td><a href="../emergence-violations-need-in-stream-sampling/">#124 Emergence-class 違規規則化不了、要 stage 內抽樣</a></td>
          <td>三類分法擴展 — 本卡是 2 類分法（字面 / 行為）、#124 擴展為 3 類（字面 / 結構 / emergence）並補 timing 軸；emergence 是行為層中跨檔 / 跨樣本才浮現的子類</td>
      </tr>
  </tbody>
</table>
<p>本卡是 #72 的 sibling / 補強 — #72 推 L3-L5 結構性對策最強、本卡指出 L5 也有 ceiling、不是萬能。組合解：<strong>字面用 L5 hook、行為用 L4 pair + multi-pass</strong>。#124 進一步把行為層細分出 emergence 子類、補上對應 enforcement 時機。</p>
<hr>
<h2 id="套用到本系統的-case">套用到本系統的 case</h2>
<h3 id="case-1卡片系統本身">Case 1：卡片系統本身</h3>
<p><code>mdtools fmt --fix</code> 是 hook（字面）— 處理 frontmatter、table 對齊、檔名 slug。
卡片內容對不對、抽 meta 抽得對不對 = 行為錯誤 — 靠 spiral 浮現（<a href="../cards-as-living-system-iteration/">#81</a>）、不靠 hook。</p>
<h3 id="case-2搜尋頁-bug">Case 2：搜尋頁 bug</h3>
<p>CI 跑 playwright = 字面測試（給定輸入、output 是否符合）。
但「filter mode 切換有沒有 silent failure」這個 bug 一開始連 test case 都沒列、是 user 回報才浮現 — multi-pass dogfood 才 catch 到。</p>
<h3 id="case-3決策對話-collapse">Case 3：決策對話 collapse</h3>
<p>Hook 寫不出「這個回應 collapse 到 yes/no」的規則（語意理解）。
靠 reference 的 self-check + dogfood 例子 + 對話中 user 反饋的 multi-pass 才能 catch。</p>
<p>每個 case 都驗證同一條：<strong>字面層工具有用、但 ceiling 明確；行為層需要 multi-pass、不靠攔截</strong>。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>想加 hook 防某個重複出現的問題</td>
          <td>先問「是字面還是行為？」、行為的話別寫 hook</td>
      </tr>
      <tr>
          <td>寫了 hook 規則但例外越來越多</td>
          <td>ceiling 到了、改 review</td>
      </tr>
      <tr>
          <td>「CI 通過 = 沒事」這個信念</td>
          <td>檢查 CI 範圍、行為錯誤可能漏接</td>
      </tr>
      <tr>
          <td>同類錯誤不斷以新形狀出現</td>
          <td>行為錯誤、hook 無解、補 multi-pass</td>
      </tr>
      <tr>
          <td>第 1 輪做完就 ship、沒第 2 輪</td>
          <td>假設一次寫對、多半會漏行為錯誤</td>
      </tr>
      <tr>
          <td>多輪 review 每輪用同樣 frame</td>
          <td>角度沒換、後續輪 = 重跑前輪、不會新發現</td>
      </tr>
      <tr>
          <td>「下次注意」當作驗證</td>
          <td>L1 紀律、不是 L4 結構、跟 <a href="../external-trigger-for-high-roi-work/">#72</a> 同病</td>
      </tr>
      <tr>
          <td>行為錯誤反覆出現、但「再加條 hook 規則」</td>
          <td>換工具、不是換規則</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：驗證手段的 ROI = 跟錯誤層次對齊 × 不超出 ceiling。<strong>Hook 不會思考、所以只能擋字面</strong>；<strong>行為錯誤需要 multi-pass spiral、用每輪不同角度收斂、不靠單次攔截</strong>。試圖用 hook 蓋 spiral 該做的工作 = 假裝有保護、實際比沒保護更危險。</p>
]]></content:encoded></item><item><title>升級 trigger 的量化設計：「不夠就升 Y」需要明確的「不夠」指標</title><link>https://tarrragon.github.io/blog/report/escalation-trigger-quantification/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/escalation-trigger-quantification/</guid><description>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>&lt;a href="../capability-gap-three-layer-escalation/">#86 三層階梯&lt;/a> 的「先 L1、不夠升 L2、再不夠升 L3」協議、最容易失敗的點是「不夠」沒量化：&lt;/p>
&lt;ul>
&lt;li>沒指標 → 永遠覺得「再觀察一下」 → &lt;a href="../external-trigger-for-high-roi-work/">#72 結構性跳過&lt;/a>&lt;/li>
&lt;li>指標模糊 → 哪天該升、哪天不該、無共識&lt;/li>
&lt;li>指標太鬆 → 永遠不升、L1 一直撐到崩&lt;/li>
&lt;li>指標太嚴 → 一個小波動就升、過度工程&lt;/li>
&lt;/ul>
&lt;p>正確設計：&lt;strong>L1 ship 時就同步定 L2 升級的 trigger 條件&lt;/strong> — 閾值、觀測窗口、決策週期、誰負責決策。不是 ship 後再想。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼再觀察一下永遠不會升級">為什麼「再觀察一下」永遠不會升級&lt;/h2>
&lt;p>「ship L1 → 看效果 → 不夠就升 L2」這個 plan 在沒量化時、實際發生的是：&lt;/p>
&lt;ol>
&lt;li>L1 ship、everyone 開心&lt;/li>
&lt;li>偶爾有 user 抱怨、但「不知道是不是夠多」&lt;/li>
&lt;li>沒有明確 baseline、無法判斷「不夠」&lt;/li>
&lt;li>「再觀察一下」變固定回應&lt;/li>
&lt;li>半年過去、L2 沒 ship&lt;/li>
&lt;li>同類 capability gap 在第 N 個 feature 又發生&lt;/li>
&lt;li>「我們系統設計就這樣」變新 baseline&lt;/li>
&lt;/ol>
&lt;p>這是 &lt;a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發&lt;/a> 的具體 case — 升級是 L4（外部觸發）需要的工作、靠紀律失敗。&lt;/p>
&lt;hr>
&lt;h2 id="升級-trigger-的四元素">升級 trigger 的四元素&lt;/h2>
&lt;p>完整的升級 trigger 含四個元素：&lt;/p>
&lt;h3 id="1-metric量什麼">1. Metric（量什麼）&lt;/h3>
&lt;p>具體可量化的數字、不是模糊「使用者體驗」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Bad metric&lt;/th>
 &lt;th>Good metric&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Search prefix-only&lt;/td>
 &lt;td>&amp;ldquo;user 抱怨&amp;rdquo;&lt;/td>
 &lt;td>Empty result 率（query 結果為 0 的比例）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cache miss&lt;/td>
 &lt;td>&amp;ldquo;感覺很慢&amp;rdquo;&lt;/td>
 &lt;td>P95 latency、cache hit ratio&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retry exhaustion&lt;/td>
 &lt;td>&amp;ldquo;偶爾失敗&amp;rdquo;&lt;/td>
 &lt;td>Retry-then-fail 率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Stale data&lt;/td>
 &lt;td>&amp;ldquo;user 困惑&amp;rdquo;&lt;/td>
 &lt;td>Manual refresh 觸發率&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Metric 必須：&lt;/p>
&lt;ul>
&lt;li>數值化（有單位、有 baseline）&lt;/li>
&lt;li>自動量測（不靠 manual 收集）&lt;/li>
&lt;li>跟 capability gap 直接相關（不是 proxy 的 proxy）&lt;/li>
&lt;/ul>
&lt;h3 id="2-threshold什麼程度算不夠">2. Threshold（什麼程度算「不夠」）&lt;/h3>
&lt;p>明確閾值、寫進 plan：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Trigger：當 search empty result 率 &amp;gt; 15% 持續 2 週、升級 L2（C1 fallback）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Trigger：當 L2 ship 後 fallback 觸發率 &amp;gt; 30%、升級 L3（B1 build-time tokenize）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>閾值不是猜的、要 justify：&lt;/p>
&lt;ul>
&lt;li>從 baseline 推（現況 X、目標 Y、threshold = 中間某點）&lt;/li>
&lt;li>從業務 SLA 推（acceptable miss rate）&lt;/li>
&lt;li>從成本曲線推（升級成本 = 維持成本）&lt;/li>
&lt;/ul>
&lt;h3 id="3-window觀察多久">3. Window（觀察多久）&lt;/h3>
&lt;p>避免「一個 spike 就升」、也避免「永遠等」：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Metric 性質&lt;/th>
 &lt;th>適合 window&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>高頻 query（每天千次）&lt;/td>
 &lt;td>1-7 天&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中頻（每天百次）&lt;/td>
 &lt;td>2-4 週&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低頻（每天個位數）&lt;/td>
 &lt;td>1-3 月&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>偶發 incident&lt;/td>
 &lt;td>累積計數而非時間 window&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Window 太短 = noise 主導、太長 = 真問題拖太久。&lt;/p></description><content:encoded><![CDATA[<h2 id="結論">結論</h2>
<p><a href="../capability-gap-three-layer-escalation/">#86 三層階梯</a> 的「先 L1、不夠升 L2、再不夠升 L3」協議、最容易失敗的點是「不夠」沒量化：</p>
<ul>
<li>沒指標 → 永遠覺得「再觀察一下」 → <a href="../external-trigger-for-high-roi-work/">#72 結構性跳過</a></li>
<li>指標模糊 → 哪天該升、哪天不該、無共識</li>
<li>指標太鬆 → 永遠不升、L1 一直撐到崩</li>
<li>指標太嚴 → 一個小波動就升、過度工程</li>
</ul>
<p>正確設計：<strong>L1 ship 時就同步定 L2 升級的 trigger 條件</strong> — 閾值、觀測窗口、決策週期、誰負責決策。不是 ship 後再想。</p>
<hr>
<h2 id="為什麼再觀察一下永遠不會升級">為什麼「再觀察一下」永遠不會升級</h2>
<p>「ship L1 → 看效果 → 不夠就升 L2」這個 plan 在沒量化時、實際發生的是：</p>
<ol>
<li>L1 ship、everyone 開心</li>
<li>偶爾有 user 抱怨、但「不知道是不是夠多」</li>
<li>沒有明確 baseline、無法判斷「不夠」</li>
<li>「再觀察一下」變固定回應</li>
<li>半年過去、L2 沒 ship</li>
<li>同類 capability gap 在第 N 個 feature 又發生</li>
<li>「我們系統設計就這樣」變新 baseline</li>
</ol>
<p>這是 <a href="../external-trigger-for-high-roi-work/">#72 高 ROI 無外部觸發</a> 的具體 case — 升級是 L4（外部觸發）需要的工作、靠紀律失敗。</p>
<hr>
<h2 id="升級-trigger-的四元素">升級 trigger 的四元素</h2>
<p>完整的升級 trigger 含四個元素：</p>
<h3 id="1-metric量什麼">1. Metric（量什麼）</h3>
<p>具體可量化的數字、不是模糊「使用者體驗」：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Bad metric</th>
          <th>Good metric</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Search prefix-only</td>
          <td>&ldquo;user 抱怨&rdquo;</td>
          <td>Empty result 率（query 結果為 0 的比例）</td>
      </tr>
      <tr>
          <td>Cache miss</td>
          <td>&ldquo;感覺很慢&rdquo;</td>
          <td>P95 latency、cache hit ratio</td>
      </tr>
      <tr>
          <td>Retry exhaustion</td>
          <td>&ldquo;偶爾失敗&rdquo;</td>
          <td>Retry-then-fail 率</td>
      </tr>
      <tr>
          <td>Stale data</td>
          <td>&ldquo;user 困惑&rdquo;</td>
          <td>Manual refresh 觸發率</td>
      </tr>
  </tbody>
</table>
<p>Metric 必須：</p>
<ul>
<li>數值化（有單位、有 baseline）</li>
<li>自動量測（不靠 manual 收集）</li>
<li>跟 capability gap 直接相關（不是 proxy 的 proxy）</li>
</ul>
<h3 id="2-threshold什麼程度算不夠">2. Threshold（什麼程度算「不夠」）</h3>
<p>明確閾值、寫進 plan：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Trigger：當 search empty result 率 &gt; 15% 持續 2 週、升級 L2（C1 fallback）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Trigger：當 L2 ship 後 fallback 觸發率 &gt; 30%、升級 L3（B1 build-time tokenize）</span></span></code></pre></div><p>閾值不是猜的、要 justify：</p>
<ul>
<li>從 baseline 推（現況 X、目標 Y、threshold = 中間某點）</li>
<li>從業務 SLA 推（acceptable miss rate）</li>
<li>從成本曲線推（升級成本 = 維持成本）</li>
</ul>
<h3 id="3-window觀察多久">3. Window（觀察多久）</h3>
<p>避免「一個 spike 就升」、也避免「永遠等」：</p>
<table>
  <thead>
      <tr>
          <th>Metric 性質</th>
          <th>適合 window</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>高頻 query（每天千次）</td>
          <td>1-7 天</td>
      </tr>
      <tr>
          <td>中頻（每天百次）</td>
          <td>2-4 週</td>
      </tr>
      <tr>
          <td>低頻（每天個位數）</td>
          <td>1-3 月</td>
      </tr>
      <tr>
          <td>偶發 incident</td>
          <td>累積計數而非時間 window</td>
      </tr>
  </tbody>
</table>
<p>Window 太短 = noise 主導、太長 = 真問題拖太久。</p>
<h3 id="4-decision-cadence誰何時how-決策">4. Decision cadence（誰、何時、how 決策）</h3>
<p>「達到 threshold」不該是「自動升級」、是「自動觸發 review」：</p>
<table>
  <thead>
      <tr>
          <th>元素</th>
          <th>設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發點</td>
          <td>Threshold 達到時系統自動 alert / 開 issue</td>
      </tr>
      <tr>
          <td>決策者</td>
          <td>預先指定（feature owner / tech lead）</td>
      </tr>
      <tr>
          <td>決策週期</td>
          <td>每月 review / 每 incident review</td>
      </tr>
      <tr>
          <td>決策 output</td>
          <td>&ldquo;升級 / 不升級 + 理由&rdquo;、寫進 log</td>
      </tr>
  </tbody>
</table>
<p>關鍵：<strong>決策動作有人擁有、有頻率</strong>、不靠「想到再看」。</p>
<hr>
<h2 id="l1-ship-時就定-trigger-的範本">L1 ship 時就定 trigger 的範本</h2>
<p>寫 L1 plan 時、同時寫：</p>





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





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># D + C1 (ship together)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">strategy</span><span class="p">:</span><span class="w"> </span><span class="l">L1 UX hint + L2 title-only substring fallback</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="nt">metric</span><span class="p">:</span><span class="w"> </span><span class="l">search empty-result rate, fallback hit rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="nt">baseline</span><span class="p">:</span><span class="w"> </span><span class="l">TBD (instrument at ship time)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="c"># B1 trigger (defined now)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="nt">trigger_metric</span><span class="p">:</span><span class="w"> </span><span class="l">fallback hit rate (C1)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="nt">trigger_threshold</span><span class="p">:</span><span class="w"> </span><span class="l">&gt; 30% sustained for 4 weeks</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">       </span><span class="l">OR full-content fallback request from user (manual signal)</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="nt">trigger_owner</span><span class="p">:</span><span class="w"> </span><span class="l">你（個人 blog 沒 team）</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="nt">trigger_action</span><span class="p">:</span><span class="w"> </span><span class="l">實作 Hugo template suffix tokens (B1)</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="nt">trigger_review_cadence</span><span class="p">:</span><span class="w"> </span><span class="l">每月 review search analytics</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c"># 降級 trigger（補強 #86）</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="nt">degrade_metric</span><span class="p">:</span><span class="w"> </span><span class="l">B1 maintenance cost / build pipeline complexity</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="nt">degrade_signal</span><span class="p">:</span><span class="w"> </span><span class="l">升級 Pagefind / Hugo 時 B1 broken 第 N 次</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="nt">degrade_action</span><span class="p">:</span><span class="w"> </span><span class="l">revisit 是否該換 search engine（換工具 vs 維 transformation）</span></span></span></code></pre></div><p><strong>Pre-ship 把 trigger 寫好</strong> = ship L1 時 L2 / L3 都「上膛」。下次 review 看數據、自動知道該不該升。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Plan 寫「ship 後再看」沒 trigger</td>
          <td>補 trigger</td>
      </tr>
      <tr>
          <td>「再觀察一下」第 3 次出現</td>
          <td>量化 trigger 不夠、明確閾值</td>
      </tr>
      <tr>
          <td>Metric 是「user 抱怨數」</td>
          <td>補可量化指標、別只靠 anecdote</td>
      </tr>
      <tr>
          <td>Threshold 沒 baseline 對比</td>
          <td>量現況、justify threshold</td>
      </tr>
      <tr>
          <td>達到 threshold 但沒人 act</td>
          <td>Trigger 沒 owner、補</td>
      </tr>
      <tr>
          <td>Window 太短、被 spike 觸發</td>
          <td>加 window、要求持續</td>
      </tr>
      <tr>
          <td>L1 ship 後沒重看 trigger</td>
          <td>設 cadence、定期 review</td>
      </tr>
      <tr>
          <td>「達到 trigger 太久才執行」</td>
          <td>ETA 沒寫、補</td>
      </tr>
  </tbody>
</table>
<p><strong>核心</strong>：升級 trigger 的設計<strong>跟 ship plan 同步寫、不是 ship 後才想</strong>。沒 trigger = 不會升級 = capability gap 永遠在 L1 撐住。<strong>「再觀察一下」是缺 trigger 的訊號、不是「我謹慎」的訊號</strong>。</p>
]]></content:encoded></item><item><title>工具的預設行為決定使用者習慣 — 從版本錯置看工具設計的 opinion 責任</title><link>https://tarrragon.github.io/blog/work-log/%E5%B7%A5%E5%85%B7%E7%9A%84%E9%A0%90%E8%A8%AD%E8%A1%8C%E7%82%BA%E6%B1%BA%E5%AE%9A%E4%BD%BF%E7%94%A8%E8%80%85%E7%BF%92%E6%85%A3-%E5%BE%9E%E7%89%88%E6%9C%AC%E9%8C%AF%E7%BD%AE%E7%9C%8B%E5%B7%A5%E5%85%B7%E8%A8%AD%E8%A8%88%E7%9A%84-opinion-%E8%B2%AC%E4%BB%BB/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E5%B7%A5%E5%85%B7%E7%9A%84%E9%A0%90%E8%A8%AD%E8%A1%8C%E7%82%BA%E6%B1%BA%E5%AE%9A%E4%BD%BF%E7%94%A8%E8%80%85%E7%BF%92%E6%85%A3-%E5%BE%9E%E7%89%88%E6%9C%AC%E9%8C%AF%E7%BD%AE%E7%9C%8B%E5%B7%A5%E5%85%B7%E8%A8%AD%E8%A8%88%E7%9A%84-opinion-%E8%B2%AC%E4%BB%BB/</guid><description>&lt;p>這篇從一個版本錯置的經驗出發，討論工具設計中一個容易忽略的面向：工具接受自由輸入時，預設路徑如何影響使用者的決策。適用於 CLI、API、表單、自動化流程——任何需要使用者做選擇的介面。&lt;/p>
&lt;hr>
&lt;h2 id="背景我們怎麼管理版本和工作項目">背景：我們怎麼管理版本和工作項目&lt;/h2>
&lt;p>我們的專案用 semver（語意化版本）管理發布節奏。每個版本（如 v0.3.0）有明確的功能範圍，由數個提案定義——每個提案描述一組要交付的功能和邊界。版本內部再拆成多個工作項目（ticket），按批次排序執行（類似 Sprint，但以依賴順序而非時間框分批）。&lt;/p>
&lt;p>版本的生命週期很單純：&lt;code>planned → active → completed&lt;/code>。一個版本的所有 ticket 完成後，跑發布流程、打 tag、標記 completed。&lt;/p>
&lt;p>圍繞這個流程，我們自建了兩個 CLI 工具：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>ticket create&lt;/code>&lt;/td>
 &lt;td>建立工作項目，指定歸屬版本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>version-release&lt;/code>&lt;/td>
 &lt;td>版本發布（pre-flight 檢查、文件更新、打 tag）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這兩個工具在設計時，都選擇了「彈性優先」——接受任何合法輸入，不對使用者的選擇做判斷。&lt;/p>
&lt;p>這個選擇在後來被證明是錯的。&lt;/p>
&lt;h2 id="版本語意大版本和小版本的分工">版本語意：大版本和小版本的分工&lt;/h2>
&lt;p>semver 的 &lt;code>MAJOR.MINOR.PATCH&lt;/code> 有明確的語意分工：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>語意&lt;/th>
 &lt;th>觸發條件&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>MAJOR（0.x → 1.0）&lt;/td>
 &lt;td>不相容的 API 變更&lt;/td>
 &lt;td>破壞既有介面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>MINOR（0.3 → 0.4）&lt;/td>
 &lt;td>新功能&lt;/td>
 &lt;td>新增向後相容功能&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PATCH（0.3.0 → 0.3.1）&lt;/td>
 &lt;td>修復和改善&lt;/td>
 &lt;td>bug fix（我們擴充涵蓋重構和流程改善）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>版本號不只是標記——它決定了&lt;strong>工作項目應該放在哪裡&lt;/strong>。一個 bug fix 放進 MINOR 版本，語意上等於說「這個 bug fix 和下一批新功能綁定發布」——多數情況下這不是你想要的。&lt;/p>
&lt;p>版本管理只是其中一個場景——任何接受自由輸入的內部工具，只要輸入涉及分類或歸屬判斷，都可能有同樣的問題。我們的工具沒有表達這個語意，接下來的兩個事件是後果。&lt;/p>
&lt;h2 id="事件一改善類工作放進了新功能版本">事件一：改善類工作放進了新功能版本&lt;/h2>
&lt;p>v0.3.0 發布了三個新功能。發布後的版本檢討發現了一個測試隔離問題，v0.3.1 做了 hotfix。&lt;/p>
&lt;p>接下來要做根因分析和系統性防護。建立工作項目時，順手指定了 &lt;code>--version 0.4.0&lt;/code>——v0.3.0 和 v0.3.1 都已發布，v0.4.0 是下一個功能版本，看起來是合理的選擇。&lt;/p>
&lt;p>CLI 接受了這個輸入，沒有任何提示。&lt;/p>
&lt;p>三張改善類的工作項目（根因分析、重構、規則文件）就這樣和 PostgreSQL Storage Backend（v0.4.0 的核心功能）混在一起。直到使用者檢視版本看板時才發現不對——改善類工作和新功能綁在同一個發布週期，語意混亂。&lt;/p>
&lt;p>修正方式：建立 v0.3.2、遷移三張 ticket、重新發布。額外花了一輪操作成本。&lt;/p>
&lt;h2 id="事件二已完成版本的幽靈">事件二：已完成版本的幽靈&lt;/h2>
&lt;p>版本看板的異常不止一處。同一次檢視中，看板顯示 v0.2.0 有未完成任務。&lt;/p>
&lt;p>查證後發現 v0.2.0（38 張 ticket 全部完成）、v0.2.1（7 張全完成）、v0.2.2（1 張已結案）三個版本在版本清單中仍標記為 &lt;code>active&lt;/code>。它們在數個月前就該標為 &lt;code>completed&lt;/code>，但沒有。&lt;/p>
&lt;p>原因是版本發布工具的 pre-flight 檢查只看「當前版本的 ticket 是否完成」，不掃描「更早的版本是否有 active 殘留」。早期版本可能是手動發布的，跳過了狀態同步步驟。工具沒有補救機制，殘留就一直留著。&lt;/p>
&lt;p>看板靜默地把這些版本顯示為「有未完成工作」，產生誤導。&lt;/p>
&lt;h2 id="為什麼會這樣工具沒有-opinion">為什麼會這樣：工具沒有 opinion&lt;/h2>
&lt;p>兩個事件的共通根因：&lt;strong>工具在應該有立場的地方選擇了沉默。&lt;/strong>&lt;/p>
&lt;h3 id="建立工作項目時">建立工作項目時&lt;/h3>
&lt;p>&lt;code>ticket create --version 0.4.0 --type ANA --action &amp;quot;分析&amp;quot;&lt;/code> — 工具知道這是一張分析類的 ticket，也知道 v0.4.0 的 scope 是 PostgreSQL Storage。但它不認為自己有責任判斷「分析類 ticket 放在新功能版本是否合理」。它只做格式驗證：版本號存在嗎？通過就建立。&lt;/p>
&lt;h3 id="發布版本時">發布版本時&lt;/h3>
&lt;p>發布工具的盲區更隱蔽。每次發布時，工具會檢查「這個版本的所有工作項目都完成了嗎？」——如果答案是「是」，就繼續打 tag、更新文件、推送。但它從不回頭看更早的版本：有沒有哪個舊版本的工作項目早已全部完成，卻一直沒被標記為「已完成」？這種殘留不影響當前發布，但會讓看板持續顯示「舊版本有未完成工作」，誤導每一個後續查看看板的人。&lt;/p>
&lt;p>兩者都是「工具做了它被要求做的事，但沒做它應該做的事」。&lt;/p>
&lt;h2 id="工具什麼時候應該有-opinion">工具什麼時候應該有 opinion？&lt;/h2>
&lt;p>不是所有情境都需要工具有立場。有一個簡單的判斷標準：&lt;/p>
&lt;blockquote>
&lt;p>當存在一個「多數情況下正確的預設行為」時，工具應該把它表達出來。使用者可以覆蓋，但預設路徑應該引導正確做法。&lt;/p>&lt;/blockquote>
&lt;p>這裡的 opinion 是&lt;strong>建議而非阻擋&lt;/strong>——工具提示預設路徑，使用者可以覆蓋。這個區分很重要：阻擋式的 opinion（必須額外操作才能繞過）適合風險高的操作（如 force push to main、刪除生產資料）；建議式的 opinion 適合歸屬判斷。錯誤成本不對稱決定了形式：建議錯了，使用者覆蓋一次，幾秒鐘；沉默錯了，事後修正，幾小時。只要建議的正確率不是極低，建議就比沉默划算。&lt;/p>
&lt;p>這個邏輯不限於 CLI。API 的預設參數、表單的預選值、自動化流程的預設路由——任何使用者需要做選擇的介面，都有機會用預設行為表達 opinion。&lt;/p>
&lt;p>改善類 ticket 放 patch 版本，在多數情況下是正確的。「多數情況下對」已經足夠讓工具表達立場：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="err">$&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ticket&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">create&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">--type IMP --action &amp;#34;修復&amp;#34; --target &amp;#34;retry test&amp;#34;
&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="p">[&lt;/span>&lt;span class="err">建議&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">此&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ticket&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">為修復類，建議放&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">v0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="err">（&lt;/span>&lt;span class="n">patch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">bump&lt;/span>&lt;span class="err">）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="err">而非&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">v0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="err">（下一個功能版本）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="err">使用&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">--version 覆蓋此建議&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>前版本 status 掃描也是。已完成版本仍為 active 在所有情況下都是異常——工具不需要猜，只需要報告：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">$ version-release check
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">[WARN] v0.2.0：38 張 ticket 全部完成但 status 仍為 active&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="為什麼使用者是-ai-agent-時問題更嚴重">為什麼使用者是 AI agent 時問題更嚴重&lt;/h2>
&lt;p>這個 pattern 在人類使用者身上已經存在——人類也會走阻力最小的路徑。但人類有跨次記憶：「上次放錯版本被糾正過，這次注意一下。」&lt;/p></description><content:encoded><![CDATA[<p>這篇從一個版本錯置的經驗出發，討論工具設計中一個容易忽略的面向：工具接受自由輸入時，預設路徑如何影響使用者的決策。適用於 CLI、API、表單、自動化流程——任何需要使用者做選擇的介面。</p>
<hr>
<h2 id="背景我們怎麼管理版本和工作項目">背景：我們怎麼管理版本和工作項目</h2>
<p>我們的專案用 semver（語意化版本）管理發布節奏。每個版本（如 v0.3.0）有明確的功能範圍，由數個提案定義——每個提案描述一組要交付的功能和邊界。版本內部再拆成多個工作項目（ticket），按批次排序執行（類似 Sprint，但以依賴順序而非時間框分批）。</p>
<p>版本的生命週期很單純：<code>planned → active → completed</code>。一個版本的所有 ticket 完成後，跑發布流程、打 tag、標記 completed。</p>
<p>圍繞這個流程，我們自建了兩個 CLI 工具：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ticket create</code></td>
          <td>建立工作項目，指定歸屬版本</td>
      </tr>
      <tr>
          <td><code>version-release</code></td>
          <td>版本發布（pre-flight 檢查、文件更新、打 tag）</td>
      </tr>
  </tbody>
</table>
<p>這兩個工具在設計時，都選擇了「彈性優先」——接受任何合法輸入，不對使用者的選擇做判斷。</p>
<p>這個選擇在後來被證明是錯的。</p>
<h2 id="版本語意大版本和小版本的分工">版本語意：大版本和小版本的分工</h2>
<p>semver 的 <code>MAJOR.MINOR.PATCH</code> 有明確的語意分工：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>語意</th>
          <th>觸發條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MAJOR（0.x → 1.0）</td>
          <td>不相容的 API 變更</td>
          <td>破壞既有介面</td>
      </tr>
      <tr>
          <td>MINOR（0.3 → 0.4）</td>
          <td>新功能</td>
          <td>新增向後相容功能</td>
      </tr>
      <tr>
          <td>PATCH（0.3.0 → 0.3.1）</td>
          <td>修復和改善</td>
          <td>bug fix（我們擴充涵蓋重構和流程改善）</td>
      </tr>
  </tbody>
</table>
<p>版本號不只是標記——它決定了<strong>工作項目應該放在哪裡</strong>。一個 bug fix 放進 MINOR 版本，語意上等於說「這個 bug fix 和下一批新功能綁定發布」——多數情況下這不是你想要的。</p>
<p>版本管理只是其中一個場景——任何接受自由輸入的內部工具，只要輸入涉及分類或歸屬判斷，都可能有同樣的問題。我們的工具沒有表達這個語意，接下來的兩個事件是後果。</p>
<h2 id="事件一改善類工作放進了新功能版本">事件一：改善類工作放進了新功能版本</h2>
<p>v0.3.0 發布了三個新功能。發布後的版本檢討發現了一個測試隔離問題，v0.3.1 做了 hotfix。</p>
<p>接下來要做根因分析和系統性防護。建立工作項目時，順手指定了 <code>--version 0.4.0</code>——v0.3.0 和 v0.3.1 都已發布，v0.4.0 是下一個功能版本，看起來是合理的選擇。</p>
<p>CLI 接受了這個輸入，沒有任何提示。</p>
<p>三張改善類的工作項目（根因分析、重構、規則文件）就這樣和 PostgreSQL Storage Backend（v0.4.0 的核心功能）混在一起。直到使用者檢視版本看板時才發現不對——改善類工作和新功能綁在同一個發布週期，語意混亂。</p>
<p>修正方式：建立 v0.3.2、遷移三張 ticket、重新發布。額外花了一輪操作成本。</p>
<h2 id="事件二已完成版本的幽靈">事件二：已完成版本的幽靈</h2>
<p>版本看板的異常不止一處。同一次檢視中，看板顯示 v0.2.0 有未完成任務。</p>
<p>查證後發現 v0.2.0（38 張 ticket 全部完成）、v0.2.1（7 張全完成）、v0.2.2（1 張已結案）三個版本在版本清單中仍標記為 <code>active</code>。它們在數個月前就該標為 <code>completed</code>，但沒有。</p>
<p>原因是版本發布工具的 pre-flight 檢查只看「當前版本的 ticket 是否完成」，不掃描「更早的版本是否有 active 殘留」。早期版本可能是手動發布的，跳過了狀態同步步驟。工具沒有補救機制，殘留就一直留著。</p>
<p>看板靜默地把這些版本顯示為「有未完成工作」，產生誤導。</p>
<h2 id="為什麼會這樣工具沒有-opinion">為什麼會這樣：工具沒有 opinion</h2>
<p>兩個事件的共通根因：<strong>工具在應該有立場的地方選擇了沉默。</strong></p>
<h3 id="建立工作項目時">建立工作項目時</h3>
<p><code>ticket create --version 0.4.0 --type ANA --action &quot;分析&quot;</code> — 工具知道這是一張分析類的 ticket，也知道 v0.4.0 的 scope 是 PostgreSQL Storage。但它不認為自己有責任判斷「分析類 ticket 放在新功能版本是否合理」。它只做格式驗證：版本號存在嗎？通過就建立。</p>
<h3 id="發布版本時">發布版本時</h3>
<p>發布工具的盲區更隱蔽。每次發布時，工具會檢查「這個版本的所有工作項目都完成了嗎？」——如果答案是「是」，就繼續打 tag、更新文件、推送。但它從不回頭看更早的版本：有沒有哪個舊版本的工作項目早已全部完成，卻一直沒被標記為「已完成」？這種殘留不影響當前發布，但會讓看板持續顯示「舊版本有未完成工作」，誤導每一個後續查看看板的人。</p>
<p>兩者都是「工具做了它被要求做的事，但沒做它應該做的事」。</p>
<h2 id="工具什麼時候應該有-opinion">工具什麼時候應該有 opinion？</h2>
<p>不是所有情境都需要工具有立場。有一個簡單的判斷標準：</p>
<blockquote>
<p>當存在一個「多數情況下正確的預設行為」時，工具應該把它表達出來。使用者可以覆蓋，但預設路徑應該引導正確做法。</p></blockquote>
<p>這裡的 opinion 是<strong>建議而非阻擋</strong>——工具提示預設路徑，使用者可以覆蓋。這個區分很重要：阻擋式的 opinion（必須額外操作才能繞過）適合風險高的操作（如 force push to main、刪除生產資料）；建議式的 opinion 適合歸屬判斷。錯誤成本不對稱決定了形式：建議錯了，使用者覆蓋一次，幾秒鐘；沉默錯了，事後修正，幾小時。只要建議的正確率不是極低，建議就比沉默划算。</p>
<p>這個邏輯不限於 CLI。API 的預設參數、表單的預選值、自動化流程的預設路由——任何使用者需要做選擇的介面，都有機會用預設行為表達 opinion。</p>
<p>改善類 ticket 放 patch 版本，在多數情況下是正確的。「多數情況下對」已經足夠讓工具表達立場：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="err">$</span><span class="w"> </span><span class="n">ticket</span><span class="w"> </span><span class="k">create</span><span class="w"> </span><span class="c1">--type IMP --action &#34;修復&#34; --target &#34;retry test&#34;
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="p">[</span><span class="err">建議</span><span class="p">]</span><span class="w"> </span><span class="err">此</span><span class="w"> </span><span class="n">ticket</span><span class="w"> </span><span class="err">為修復類，建議放</span><span class="w"> </span><span class="n">v0</span><span class="p">.</span><span class="mi">3</span><span class="p">.</span><span class="mi">2</span><span class="err">（</span><span class="n">patch</span><span class="w"> </span><span class="n">bump</span><span class="err">）</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">       </span><span class="err">而非</span><span class="w"> </span><span class="n">v0</span><span class="p">.</span><span class="mi">4</span><span class="p">.</span><span class="mi">0</span><span class="err">（下一個功能版本）</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">       </span><span class="err">使用</span><span class="w"> </span><span class="c1">--version 覆蓋此建議</span></span></span></code></pre></div><p>前版本 status 掃描也是。已完成版本仍為 active 在所有情況下都是異常——工具不需要猜，只需要報告：</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">$ version-release check
</span></span><span class="line"><span class="ln">2</span><span class="cl">[WARN] v0.2.0：38 張 ticket 全部完成但 status 仍為 active</span></span></code></pre></div><h2 id="為什麼使用者是-ai-agent-時問題更嚴重">為什麼使用者是 AI agent 時問題更嚴重</h2>
<p>這個 pattern 在人類使用者身上已經存在——人類也會走阻力最小的路徑。但人類有跨次記憶：「上次放錯版本被糾正過，這次注意一下。」</p>
<p>AI agent 沒有這個。</p>
<p>每個 session 是一個全新的 agent，它讀到的是：版本清單中 v0.4.0 是 active、CLI 接受 <code>--version 0.4.0</code>、沒有警告。於是它每次都會用最直覺的選擇——當前 active 的最大版本。</p>
<p>上次的教訓不會自動傳遞到下次。除非教訓被固化成工具行為。</p>
<p>這把「工具應該有 opinion」從「建議做法」升級為「必要條件」：</p>
<ul>
<li><strong>人類使用者</strong>：opinion 是提醒，有助於減少錯誤</li>
<li><strong>AI agent 使用者</strong>：opinion 是最可靠的防線，因為工具在操作當下的即時引導是離決策點最近的攔截</li>
</ul>
<h2 id="工具的預設行為就是團隊的實際流程">工具的預設行為，就是團隊的實際流程</h2>
<blockquote>
<p>工具的預設行為，就是團隊的實際流程。</p></blockquote>
<p>文件上寫「改善類工作放 patch 版本」沒有用——如果工具不引導，使用者會走工具預設的路徑。人類和 AI 都是。文件說的和工具做的不一致時，工具會贏。</p>
<p>但文件不是敵人。文件定義「應該是什麼樣」，傳遞設計理由和架構決策；工具實現「實際是什麼樣」。兩者不一致時，優先修工具。</p>
<blockquote>
<p>如果你希望使用者做 X，不要寫文件說「請做 X」——把工具的預設行為設成 X。</p></blockquote>
<p>這個原則適用於所有內部工具設計，不限於版本管理：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>寫文件的做法</th>
          <th>改工具的做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>commit 前跑測試</td>
          <td>README 寫「請先跑測試」</td>
          <td>pre-commit hook 自動跑</td>
      </tr>
      <tr>
          <td>PR 描述格式</td>
          <td>貢獻指南寫範本</td>
          <td>PR template 預填結構</td>
      </tr>
      <tr>
          <td>改善放 patch 版本</td>
          <td>版本策略文件寫規則</td>
          <td>CLI 根據 ticket type 建議版本</td>
      </tr>
      <tr>
          <td>API 環境參數</td>
          <td>文件寫「production 需額外確認」</td>
          <td>API 預設 staging，production 需顯式指定</td>
      </tr>
      <tr>
          <td>表單必填欄位</td>
          <td>說明文字寫「建議填寫」</td>
          <td>欄位預設值 + 必填驗證</td>
      </tr>
  </tbody>
</table>
<p>每一個「寫文件提醒使用者遵守操作規範」都是一個信號——工具的預設行為還有空間改善。看到這個信號時，優先評估能否把提醒轉化為工具的預設行為。</p>
<p>Rails 的「Convention over Configuration」是同一個觀念的先驅表達：框架用約定引導開發者走正確路徑，省去不必要的配置決策。有 opinion 的工具在必要決策時引導方向。兩者共通的是把判斷成本從「每次使用時」前移到「設計工具時」——一次判斷，永久生效。</p>
<h2 id="回去檢查你的工具">回去檢查你的工具</h2>
<ol>
<li>列出你的工具中所有使用者需要做選擇的地方——CLI 參數、API 欄位、表單選項、流程分支</li>
<li>對每個問：有沒有「多數情況下正確」的預設值或建議值？</li>
<li>有的話，加建議式 opinion（提示預設 + 允許覆蓋）</li>
<li>檢查工具的清理路徑：有沒有前一次操作應該同步但沒有同步的狀態？</li>
<li>如果你的工具會被 AI agent 或自動化流程呼叫，上述每一項的優先級加倍——自動化沒有判斷力，它只走預設路徑</li>
</ol>
]]></content:encoded></item><item><title>10 個 Ticket、57 個綠燈、0 條追溯：從需求文件到測試的銜接檢討</title><link>https://tarrragon.github.io/blog/work-log/10-%E5%80%8B-ticket57-%E5%80%8B%E7%B6%A0%E7%87%880-%E6%A2%9D%E8%BF%BD%E6%BA%AF%E5%BE%9E%E9%9C%80%E6%B1%82%E6%96%87%E4%BB%B6%E5%88%B0%E6%B8%AC%E8%A9%A6%E7%9A%84%E9%8A%9C%E6%8E%A5%E6%AA%A2%E8%A8%8E/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/10-%E5%80%8B-ticket57-%E5%80%8B%E7%B6%A0%E7%87%880-%E6%A2%9D%E8%BF%BD%E6%BA%AF%E5%BE%9E%E9%9C%80%E6%B1%82%E6%96%87%E4%BB%B6%E5%88%B0%E6%B8%AC%E8%A9%A6%E7%9A%84%E9%8A%9C%E6%8E%A5%E6%AA%A2%E8%A8%8E/</guid><description>&lt;h2 id="這篇要解決什麼">這篇要解決什麼&lt;/h2>
&lt;blockquote>
&lt;p>57 個 unit test 全綠，但沒有任何機制能回答「這些測試覆蓋了哪些 UseCase 場景」。&lt;/p>&lt;/blockquote>
&lt;p>monitor 專案 v0.1.0 從需求文件系統（Proposal → Spec → UseCase）一路走到 Collector 實作，中間經過 BDD 測試設計、紅燈測試撰寫、骨架實作讓綠。流程表面上順暢——10 個根 Ticket 全部完成、Collector 可啟動、所有 unit test 通過。但回頭檢視發現：需求→測試的銜接是單向管道，沒有反向追溯，也沒有邊界回補流程。&lt;/p>
&lt;p>本文記錄 v0.1.0 的完整流程、發現的五個結構性差異、和落地的解決方案。&lt;/p>
&lt;hr>
&lt;h2 id="實際走過的流程">實際走過的流程&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">saas 選型訪談
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → Proposal（MVP 範圍界定）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → Spec（14 份，涵蓋 schema/ingestion/query/storage/rule-engine/SDK）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> → UseCase（5 個，UC-01 端到端事件流 ~ UC-05 Web 監控）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> → BDD 測試設計 ANA（全專案 26 個行為場景 → 整合/單元/協議測試清單）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> → 紅燈測試（9 個 Ticket 並行，72 個測試 FAIL）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> → 骨架實作（1 個 Ticket，57 個 unit test GREEN）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個箭頭都有對應的框架機制：saas→doc 有 Stage 6 銜接、doc→TDD 有 doc-handoff 映射表。但箭頭只往右——沒有任何箭頭往左。&lt;/p>
&lt;hr>
&lt;h2 id="五個結構性差異">五個結構性差異&lt;/h2>
&lt;h3 id="差異-1全專案-bdd-設計不在-tdd-phase-模型中">差異 1：「全專案 BDD 設計」不在 TDD Phase 模型中&lt;/h3>
&lt;p>TDD Skill 定義 Phase 0→1→2→3→4 的逐功能流程。v0.1.0 做的是「全專案 UseCase 一次性展開為 BDD 測試設計」，跨越 Phase 1 和 Phase 2 的邊界，粒度是專案級不是功能級。&lt;/p>
&lt;p>這不是 Phase 設計的錯——Phase 模型適合增量開發（每次加一個功能）。新專案起手是不同的工作模式：批量設計、模組群組粒度。&lt;/p>
&lt;p>&lt;strong>解法&lt;/strong>：在 doc-handoff 新增「新專案起手模式」章節，描述批量 BDD 設計流程、Phase 0 豁免條件、模組群組粒度。&lt;/p>
&lt;h3 id="差異-2紅燈測試需要存根stub">差異 2：紅燈測試需要存根（stub）&lt;/h3>
&lt;p>Go 是靜態語言，&lt;code>go test&lt;/code> 必須編譯通過才能執行。紅燈測試引用的 type/interface 不存在時直接編譯失敗，不是「測試 FAIL」。&lt;/p>
&lt;p>TDD Skill 的 Phase 2 說「設計測試」、Phase 3b 說「讓測試綠」，但中間的「建存根讓測試可紅」沒有定義。&lt;/p>
&lt;p>&lt;strong>實作驗證&lt;/strong>：v0.1.0 的每個紅燈 Ticket 都自帶建立存根（空 function return nil / 空 struct / 回 501 的 HTTP handler），存根讓 &lt;code>go test&lt;/code> 編譯通過，合法測試 PASS、非法測試 FAIL = 紅燈狀態。&lt;/p>
&lt;p>&lt;strong>解法&lt;/strong>：Phase 3 rules 新增「存根策略」章節，涵蓋靜態語言（Go/Dart）和動態語言（Python/JS）的不同處理。&lt;/p>
&lt;h3 id="差異-3測試usecase-沒有反向追溯">差異 3：測試→UseCase 沒有反向追溯&lt;/h3>
&lt;p>寫完 57 個 unit test 後，問「UC-01 的替代場景 01a（批次部分失敗 → 207）被哪些測試覆蓋？」——沒有任何機制能回答。&lt;/p>
&lt;p>&lt;code>doc test-map UC-01&lt;/code> 工具存在但回傳 0 個測試——因為它搜尋 UC frontmatter 的 &lt;code>ticket_refs&lt;/code>，和測試檔案沒有連結。Spec 的「三方交叉比對」是建 Ticket 時的一次性動作，不是持續追溯。&lt;/p>
&lt;p>&lt;strong>解法&lt;/strong>：建立 &lt;code>docs/traceability.yaml&lt;/code> 追溯矩陣，三層追溯（UC 場景 → 整合測試 IT-* → 單元測試 UT-* → Spec FR）。每個 entry 標記 &lt;code>covered&lt;/code> / &lt;code>gap&lt;/code> / &lt;code>deferred&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<h2 id="這篇要解決什麼">這篇要解決什麼</h2>
<blockquote>
<p>57 個 unit test 全綠，但沒有任何機制能回答「這些測試覆蓋了哪些 UseCase 場景」。</p></blockquote>
<p>monitor 專案 v0.1.0 從需求文件系統（Proposal → Spec → UseCase）一路走到 Collector 實作，中間經過 BDD 測試設計、紅燈測試撰寫、骨架實作讓綠。流程表面上順暢——10 個根 Ticket 全部完成、Collector 可啟動、所有 unit test 通過。但回頭檢視發現：需求→測試的銜接是單向管道，沒有反向追溯，也沒有邊界回補流程。</p>
<p>本文記錄 v0.1.0 的完整流程、發現的五個結構性差異、和落地的解決方案。</p>
<hr>
<h2 id="實際走過的流程">實際走過的流程</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">saas 選型訪談
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → Proposal（MVP 範圍界定）
</span></span><span class="line"><span class="ln">3</span><span class="cl">    → Spec（14 份，涵蓋 schema/ingestion/query/storage/rule-engine/SDK）
</span></span><span class="line"><span class="ln">4</span><span class="cl">      → UseCase（5 個，UC-01 端到端事件流 ~ UC-05 Web 監控）
</span></span><span class="line"><span class="ln">5</span><span class="cl">        → BDD 測試設計 ANA（全專案 26 個行為場景 → 整合/單元/協議測試清單）
</span></span><span class="line"><span class="ln">6</span><span class="cl">          → 紅燈測試（9 個 Ticket 並行，72 個測試 FAIL）
</span></span><span class="line"><span class="ln">7</span><span class="cl">            → 骨架實作（1 個 Ticket，57 個 unit test GREEN）</span></span></code></pre></div><p>每個箭頭都有對應的框架機制：saas→doc 有 Stage 6 銜接、doc→TDD 有 doc-handoff 映射表。但箭頭只往右——沒有任何箭頭往左。</p>
<hr>
<h2 id="五個結構性差異">五個結構性差異</h2>
<h3 id="差異-1全專案-bdd-設計不在-tdd-phase-模型中">差異 1：「全專案 BDD 設計」不在 TDD Phase 模型中</h3>
<p>TDD Skill 定義 Phase 0→1→2→3→4 的逐功能流程。v0.1.0 做的是「全專案 UseCase 一次性展開為 BDD 測試設計」，跨越 Phase 1 和 Phase 2 的邊界，粒度是專案級不是功能級。</p>
<p>這不是 Phase 設計的錯——Phase 模型適合增量開發（每次加一個功能）。新專案起手是不同的工作模式：批量設計、模組群組粒度。</p>
<p><strong>解法</strong>：在 doc-handoff 新增「新專案起手模式」章節，描述批量 BDD 設計流程、Phase 0 豁免條件、模組群組粒度。</p>
<h3 id="差異-2紅燈測試需要存根stub">差異 2：紅燈測試需要存根（stub）</h3>
<p>Go 是靜態語言，<code>go test</code> 必須編譯通過才能執行。紅燈測試引用的 type/interface 不存在時直接編譯失敗，不是「測試 FAIL」。</p>
<p>TDD Skill 的 Phase 2 說「設計測試」、Phase 3b 說「讓測試綠」，但中間的「建存根讓測試可紅」沒有定義。</p>
<p><strong>實作驗證</strong>：v0.1.0 的每個紅燈 Ticket 都自帶建立存根（空 function return nil / 空 struct / 回 501 的 HTTP handler），存根讓 <code>go test</code> 編譯通過，合法測試 PASS、非法測試 FAIL = 紅燈狀態。</p>
<p><strong>解法</strong>：Phase 3 rules 新增「存根策略」章節，涵蓋靜態語言（Go/Dart）和動態語言（Python/JS）的不同處理。</p>
<h3 id="差異-3測試usecase-沒有反向追溯">差異 3：測試→UseCase 沒有反向追溯</h3>
<p>寫完 57 個 unit test 後，問「UC-01 的替代場景 01a（批次部分失敗 → 207）被哪些測試覆蓋？」——沒有任何機制能回答。</p>
<p><code>doc test-map UC-01</code> 工具存在但回傳 0 個測試——因為它搜尋 UC frontmatter 的 <code>ticket_refs</code>，和測試檔案沒有連結。Spec 的「三方交叉比對」是建 Ticket 時的一次性動作，不是持續追溯。</p>
<p><strong>解法</strong>：建立 <code>docs/traceability.yaml</code> 追溯矩陣，三層追溯（UC 場景 → 整合測試 IT-* → 單元測試 UT-* → Spec FR）。每個 entry 標記 <code>covered</code> / <code>gap</code> / <code>deferred</code>。</p>
<h3 id="差異-4邊界條件發現後沒有回補-uc-的流程">差異 4：邊界條件發現後沒有回補 UC 的流程</h3>
<p>寫 Ingest Handler 測試時發現：「如果 POST body 不是 JSON 怎麼辦？」「如果 Content-Type 是 text/plain（sendBeacon）怎麼辦？」這些邊界在 UC-01 的場景描述中不存在。</p>
<p>測試設計的 BDD ANA 有涵蓋這些邊界場景，但 UC 文件本身沒有更新。邊界條件「住」在測試設計文件而非 UseCase——下次有人讀 UC 不會知道這些邊界存在。</p>
<p><strong>解法</strong>：追溯矩陣增加 <code>boundaries:</code> 區段，測試撰寫者發現新邊界時加 gap entry，PM 建 DOC Ticket 回補 UC/Spec。Phase 4d 掃描所有 gap 確認無遺漏。</p>
<h3 id="差異-5ticket-拆分邊界未對齊測試變綠驗收點">差異 5：Ticket 拆分邊界未對齊測試變綠驗收點</h3>
<p>Collector 實作被拆為 4 個 Ticket：骨架（interface 定義）/ Storage / Ingestion Handler / Query Handler。骨架 Ticket 指派做「main.go + Config + Storage interface」，代理人完成了所有模組實作——57 個 unit test 從紅全部變綠，其餘 3 個 Ticket 的 acceptance 全被涵蓋。</p>
<p>初看像是「代理人超額完成」，回頭用判讀三問檢查骨架 Ticket：完成後有測試變綠嗎？→ 沒有（只定義 interface）。能獨立跑測試嗎？→ 不能（其他模組引用骨架的 type）。共用 type？→ 是。三問全部指向「不應獨立拆」。<strong>根因是 Ticket 拆分設計</strong>，不是代理人行為——按 Spec FR 拆（輸入驅動）導致骨架 Ticket 完成後 0 個測試狀態改變，不是有意義的驗收點。</p>
<p><strong>判讀規則</strong>：實作 Ticket 的拆分邊界必須對齊「測試從紅變綠」的驗收點。一個 Ticket 完成後若沒有任何測試狀態改變，它不應該是獨立 Ticket。</p>
<p>判讀三問：</p>
<ol>
<li>這個 Ticket 完成後，有測試從 FAIL 變 PASS 嗎？</li>
<li>拆出的各部分能獨立跑測試嗎？</li>
<li>不同部分共用同一組 type/error/constant 嗎？</li>
</ol>
<p><strong>反模式</strong>：按 Spec FR 拆（輸入驅動）。<strong>正確做法</strong>：按「哪組測試變綠」拆（輸出驅動）。</p>
<hr>
<h2 id="追溯矩陣的設計">追溯矩陣的設計</h2>
<p>追溯矩陣是三個問題（向上追溯 + 覆蓋驗證 + 邊界回補）的統一解法。</p>
<h3 id="結構">結構</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">UC-01</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">端到端事件流</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">scenarios</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">main</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 class="nt">integration_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">IT-01-01]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">unit_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">UT-COL-01-01, UT-COL-02-01, UT-COL-04-01]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">spec_frs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">SPEC-002-FR-01, SPEC-003-FR-01]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="l">covered</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">alt-01a</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">integration_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">IT-01-02]</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">unit_tests</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">UT-COL-01-03, UT-COL-02-03]</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">spec_frs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">SPEC-002-FR-02]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="l">covered</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="nt">boundaries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">batch-limit</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="nt">discovered_during</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;ingestion-handler-red-tests&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="l">gap </span><span class="w"> </span><span class="c"># 需回補 UC/Spec</span></span></span></code></pre></div><h3 id="三個問題的對應">三個問題的對應</h3>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>矩陣欄位</th>
          <th>查法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這個 UT 為了哪個 UC？</td>
          <td><code>unit_tests</code></td>
          <td>搜尋 UT ID → 找到歸屬的 scenario</td>
      </tr>
      <tr>
          <td>UC 場景都有測試嗎？</td>
          <td><code>status</code></td>
          <td>掃描 <code>gap</code> entry</td>
      </tr>
      <tr>
          <td>新邊界怎麼回補 UC？</td>
          <td><code>boundaries</code></td>
          <td>gap entry → DOC Ticket → 回補 → covered</td>
      </tr>
  </tbody>
</table>
<h3 id="整合點">整合點</h3>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>時機</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>doc-handoff</td>
          <td>銜接時</td>
          <td>初始化矩陣骨架（UC scenario 空映射）</td>
      </tr>
      <tr>
          <td>紅燈測試撰寫</td>
          <td>Phase 2→3</td>
          <td>填入 unit_tests 映射</td>
      </tr>
      <tr>
          <td>邊界發現</td>
          <td>實作中</td>
          <td>加 boundary gap entry</td>
      </tr>
      <tr>
          <td>Phase 4d</td>
          <td>重構評估</td>
          <td>掃描所有 gap，建 DOC Ticket</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="附帶發現並行派發的-git-隔離問題">附帶發現：並行派發的 Git 隔離問題</h2>
<p>5 個代理人以 worktree 並行派發時，commit 內容交叉混入——A 代理人的 commit 包含 B 代理人的檔案。根因：主 repo 不在 main 分支，多個 worktree 共用同一分支 ref，<code>git add + commit</code> race condition。</p>
<p><strong>防護</strong>：派發前確保主 repo 在 main + 已 push。單一代理人和正確條件下的多代理人都驗證通過。</p>
<hr>
<h2 id="結論">結論</h2>
<p>v0.1.0 的流程不是失敗——Collector 可用、57 個 test GREEN。問題在於「走到終點後沒有辦法回頭驗證起點」。需求→測試的管道是單向的：Proposal 說了什麼、Spec 定了什麼 FR、UC 描述了什麼場景，和最終的測試之間沒有結構化連結。</p>
<p>追溯矩陣不增加任何程式碼——它是一個 YAML 檔案，記錄「每個測試為什麼存在」。維護成本是每次寫測試多填一行映射。回報是：任何時候都能回答「這個 UC 場景有沒有被測試保護」。</p>
]]></content:encoded></item></channel></rss>