
昨天,我們整理了這次修改的審查結果與待決事項。接下來如果要把程式交出去,我還得回答:交哪一份?它跑過哪些檢查?該找的人接受了嗎?
前面不是沒有規則。完成條件寫過了,測試跑過了,需要誰接受也討論過了。問題是:下次換一個人提交修改,這些事情還會確實做一次嗎?
今天把其中能執行的部分接成流程,最後留下發布包、版本與檢查結果。發布包就是 dotnet publish 產生的程式與必要檔案,準備部署到測試環境,還不代表已獲准正式上線。本篇實跑本機交付檢查,遠端 CI/CD 則說明接入方式。
Day 3 我把審查分成三層,Day 4 把查法留下來,Day 13 用它整理修改的接受依據。現在要把這些約定接到每次交付會經過的位置。
| 前面在哪一天約好 | 約好的事情 | 接入交付流程的位置 |
|---|---|---|
| Day 3:三層檢查與風險分流 | AI 先辨識風險;工具查基礎問題,AI 查脈絡,核心決策交給人 | 固定檢查必跑;AI 提出分流,不能解除政策要求的 Owner 審查 |
| Day 4:共用查核方法 | 不要每次從個人對話重新教一次 | 審查入口明確載入共用 skill 與政策,保存結果 |
| Day 6:完成條件 | 寫完不等於完成 | 已轉成測試的條件由 CI 驗證,其餘保留人工接受條件 |
| Day 7、9、10:背景、需求與設計 | 修改有依據,不自行補決定 | AI 審查時對照專案規則、規格與 plan.md |
| Day 11:核心規則 | 取消訂單的規則必須成立 | 核心規則測試列為必跑檢查 |
| Day 12:整合驗證 | API、狀態與通知要接得起來 | 執行整合檢查,不能只看單元測試 |
| Day 13:Owner 接受 | AI 不能替有權決定的人批准 | 必要 Reviewer 與分支保護,缺少批准不得合併 |
這張表是團隊接入的位置圖,不是全數完成的清單。本次實跑涵蓋固定檢查、失敗診斷與發布包驗證;共用 PR 審查與遠端批准還沒有接上。Day 5 的投入紀錄則留作改善依據,不因為少記幾分鐘就擋住交付。
回到本篇實跑。共同訂單服務的教學延伸版放在 examples/sdlc-delivery;baseline 保留並行取消缺口,fixed 則把讀取、判斷與寫回放進同一個鎖定範圍。歷史開發原件保留,沒有改寫成這一輪的結果。
把 Day 11 的核心規則測試、Day 12 的整合驗證放進同一個入口,Day 6 約好的完成條件才有固定的檢查位置。deliver.py 負責排好命令、保存每步輸出,失敗就停止:
| 順序 | 執行什麼 | 為什麼要有這一步 |
|---|---|---|
| 1 | ci.py:判級自檢、核心規則測試、API 整合驗證 |
重跑既有檢查,不靠人記得逐條執行 |
| 2 | verify.py:同單並行取消驗證 |
檢查多個請求同時進來,是否重複轉換與通知 |
| 3 | dotnet publish |
建立可交付的 Release 產物 |
| 4 | verify-integration.py --artifact |
直接啟動剛產生的包,確認基本操作與通知 |
ci.py 是執行順序的入口;verify-integration.py 會啟動 API 與假通知接收端,實際送請求。加上 --artifact 後,它使用指定的發布包,不重新建置另一份。
其中 routing-selftest 只證明判級程式的自我測試通過,**不是這次 PR 已完成判級,更不是 Owner 已批准。**本機腳本也不會替遠端分支政策背書。
在實驗目錄先執行修正版,名稱使用尚未存在的資料夾名稱:
python deliver.py fixed my-candidate --with-concurrency
本次保存的 candidate-checked 四步都通過:並行驗證有 81 項檢查,從包執行的 Smoke Test 有 10 項。後者檢查啟動後的基本行為,沒有把建置再算一次。結果資料夾留下 report.json、package/ 與 package-manifest.json。
這就是今天交出的東西:一份可啟動的發布包,以及能對上它的檢查結果。
只有成功還不夠。我也用保留缺口的 baseline 跑同一條流程:
python deliver.py baseline my-failure --with-concurrency --diagnose-with-claude
最後一個參數會把限定的教學程式與失敗摘要傳給 Claude 服務,產生模型費用;不加它就只執行本機檢查。
這輪二十張訂單各同時收到八個取消請求,另加 50ms 教學延遲放大競態窗口。既有 CI 通過,但每張單出現八次轉換與八份不同通知,並行檢查失敗。這是刻意放大問題的演練,不能當作線上發生率。
此時流程停止打包,自動把失敗步驟、檢查摘要與兩個限定程式檔交給 Claude。它用 claude -p、空工具表做唯讀文字分析;--safe-mode 避免額外載入個人設定,不等於作業系統沙箱。

| 觀察 | 本次結果 |
|---|---|
| 固定流程 | ci=0、concurrency=1、passed=false |
| Claude 診斷 | 自動呼叫一次,約 18.6 秒、US$0.169 |
| 後續產物 | 沒有發布包;診斷成功仍保持流程失敗 |
它自己把回覆切成三段。先看它怎麼分事實與推論(節錄自 runs/acceptance-auto-01/diagnosis-result.json,推論第 3 點略):
【事實】直接來自證據
- 轉換次數錯誤:20 張訂單的 *-transitions 全部失敗。每張都是
requests=8, changed=8, refunds=8。依 BR-04,每張單應只有 1 次轉換。
- 程式碼的讀改寫不是原子操作:store.TryGet → await Task.Delay →
Cancellation.Cancel → store.Put。OrderStore 的 lock 只各自保護
Put 和 TryGet,沒有把整段包成一個臨界區。
【推論】尚未驗證
1. 根因很可能是 check-then-act 競態。8 個並行請求都在任何一個 Put
之前讀到 Cancelled=false,所以 Transitioned() 全部回傳 true。
2. 50 ms 延遲只是放大了既有的窗口,不是缺陷的來源。因此推測
OC_TEST_DELAY_MS=0 時仍會重現,只是機率較低、次數不固定。
4. 業務影響:實際影響要看下游是否冪等,目前證據無法判斷。
【建議的下一個檢查】
挑一張單,篩出 logs.jsonl 裡 event=cancel 的紀錄,確認是否有 8 筆
result=ok, transitioned=true 且 request_id 各不相同、時間戳落在同一個
約 50 ms 的窗口內。同時對照 /metrics:oc_transitions_total 與
oc_notify_sent_total 應該是 160。
先講清楚時間順序,免得把它的功勞說大:fixed 那一版我在 18:45 就寫好、18:51 跑完四步,這次診斷是四小時後的 22:52 才跑的。它不是指導我修的人,它讀到的只有 baseline 的失敗紀錄。
有價值的是它獨立指到了同一個位置:事實段最後一條講的「store.TryGet → Task.Delay → Cancel → store.Put,lock 只各自保護 Put 和 TryGet」,正是我改掉的那一段。它沒有看到 fixed,也沒有人告訴它答案。
還有一點是我原本沒想清楚的:它不把我設的 50ms 教學延遲當成根因,推論第 2 點明講那只是「放大了既有的窗口」。如果我當時的第一反應是把延遲調小看看會不會變好,那只會讓問題變得偶發、更難查——方向是錯的。
至於推論 2 本身(沒有人為延遲是否仍會重現),本輪沒有驗,不能寫成實測。回覆最後一句是它自己加的:「此診斷未修改、未修復,也未執行任何指令。」
這裡的 Claude 是失敗處理分支,不是放行者:它讀限定輸入、交回分析,不改碼、不批准,也不會因為診斷回來就讓打包繼續。
看過一輪成功與失敗,我還想確認:流程是不是只剛好攔住這一種錯?於是另外寫了 verify-delivery-gates.py,先列好十種情境與預期停止位置,再執行。六種應該停止、四種應該繼續,這是測試設計,不是事後挑出來的成功率。
這是一個本機關卡測試套件。它在每種情境複製一份教學程式,真正執行編譯、核心規則、API 整合、打包與啟動檢查,不修改既有實驗。AI 審查狀態與 Owner 批准則使用標示為 fixture 的測試資料:驗的是收到這種狀態後會不會停,不代表 Claude 真的審了十次,或真人真的批准。
| 情境 | 預期 | 實際 | 一致? |
|---|---|---|---|
| 1. 加入無法編譯的程式 | 建置停止 | build 失敗,未打包 | 是 |
| 2. 拿掉避免重複退款要求的條件 | 核心測試停止 | domain 失敗,未打包 | 是 |
| 3. 將缺少操作人資訊的回應由 401 改成 400 | 整合檢查停止 | integration 失敗,未打包 | 是 |
| 4. 必要 AI 審查不可用(測試資料) | 等待審查,不打包 | review 阻擋 | 是 |
| 5. 核心修改沒有 Owner 接受(測試資料) | 等待批准,不打包 | owner 阻擋 | 是 |
| 6. 打包後破壞 runtime 設定 | 啟動驗證停止 | smoke 失敗,有包但不可繼續交付 | 是 |
| 7. 一般修改且必要條件齊全 | 繼續 | 檢查與發布包啟動通過 | 是 |
| 8. 核心修改且批准齊全(測試資料) | 繼續 | 檢查與發布包啟動通過 | 是 |
| 9. AI 只有不阻擋的文字建議(測試資料) | 繼續 | 沒有把小建議當重要問題阻擋 | 是 |
| 10. 恢復第 2 種情境的防重複條件 | 重新驗證後繼續 | 核心測試、整合與啟動均通過 | 是 |
結果是 六種停止、四種繼續,十種都與事先的預期一致。其中第六種最值得留下:檔案已經打包完成,程式卻啟動不了。它的 result.json 長這樣:
"steps": [ {"stage":"build","exit":0}, {"stage":"domain","exit":0},
{"stage":"integration","exit":0}, {"stage":"publish","exit":0},
{"stage":"smoke","exit":1} ],
"package_created": true,
"ready_for_load": false
package_created 是 true,ready_for_load 是 false。 前四步全綠、包也產出來了,只有啟動那一步紅——如果只看「有沒有產物」就會把它當成可交付。
讀者可以在相同實驗目錄重跑,使用新的名稱:
python verify-delivery-gates.py my-gate-check
expectations.json 保存事先預期;每個子目錄保存實際命令輸出與 result.json,總表在 report.json。套件全部符合預期才回傳 0,所以「六種預期被擋」也算測試成功。它用來驗證關卡,不是拿這個綠燈取代真正修改的 CI。
這輪驗證本機關卡的行為。 第 5 種情境使用批准測試資料,不能當成 Azure DevOps 合併保護的實測結果。
修正版的 package-manifest.json 記錄十個檔案的 SHA-256,用來確認後續拿到的是相同位元組。它能協助辨識產物,不能代替可信建置來源與存取控制。
下一篇的負載驗證直接使用這份 candidate-checked/package,啟動前後核對清單,不重新編譯後沿用這次的綠燈。讀者自己重跑時,則把 my-candidate 作為後續命令的發布包名稱。
這份教學服務仍是單一程序、記憶體儲存,沒有正式身分驗證或跨程序交易。原始報告也保留 owner_accepted=false、remote_deployed=false。它是本機檢查通過、可繼續驗證的發布包,不是已獲准正式發布的版本。
上面這些都在我這台機器上。換一個人提交修改,他會不會跑同一套?我在本機跑過測試、又改了幾行才提交,Reviewer 看到「測試通過」,也不知道那是改之前還是改之後的結果。
所以這條入口要接到 CI:由它在 PR 更新時針對這次版本執行檢查,把結果連回對應的提交,讓接手的人看得到。官方 Playbook 把 PR Review、Hook 與 CI/CD 都放在 Deploy 一起討論,提醒的正是這件事——交付不只要程式能啟動,還要知道前面的約定由誰執行、誰能讓它往下走。Anthropic Playbook
以團隊使用的 Azure DevOps 為例,接入後應該長這樣(以下是目標流程,本篇沒有建立遠端 Pipeline):

條件未滿足就停止;工具、Claude 與 Owner 各有位置。
接到 Azure DevOps 時,在目標分支設定 Build validation,讓 PR 更新後自動執行驗證,再要求必要檢查與指定 Reviewer 批准。檢查失敗或批准未齊,就不能正常完成合併。**Day 13 留給 Owner 的決定,在這裡成為合併前的條件。**合併後再由 Pipeline 打包、部署到測試環境。
這裡沿用 Day 3 的三層分工,以及 Day 4 留下的共用查核方法。前面出現的幾份文件,各有自己的工作:
CLAUDE.md:專案知識與開發慣例,讓 Claude 理解既有約定。REVIEW.md:這次審查要查什麼、哪些問題重要。自訂審查入口必須明確讀入,不能假設檔案存在就有效。審查依據接回 Day 7 的專案知識、Day 9 釐清的需求,以及 Day 10 的設計與實作計畫。訂單案例的 REVIEW.md 可以先寫得很小;下面是接入範例,並非本次診斷已使用的政策:
# 訂單取消審查政策
- 對照 spec.md 與 plan.md,指出沒有需求依據的行為變更。
- 查取消條件、重複請求與通知副作用;每項發現附位置與依據。
- 將退款與取消規則的改動送給 Domain Owner,不自行批准。
- 將行為錯誤、資料外洩與違反政策列為重要問題;未知另列。
- 不重複報告 CI 已檢查的格式問題,不把測試通過當成業務接受。
AI 交回的是發現與建議,不會因為寫了「重要」就自動擋住 PR。團隊還要定義哪些結果阻擋、哪些交人判斷,以及必要審查不可用時如何處理。這些條件要接到實際檢查狀態與保護規則。
寫下規則之後,還要指定讀取它的入口,以及不符合時停止的位置。
打包完成就可以上線了嗎 ? 明天來聊聊打包後上線前還需要注意什麼 :)
參考資料:
sdlc-delivery/runs/gates-20260928-01 保存預期、逐步命令與總表;六種停止、四種繼續。審查及批准是測試資料,並未呼叫模型或取得真人批准。sdlc-delivery/runs/acceptance-auto-01 保存限定輸入、模型回覆與失敗狀態;acceptance-auto-01-concurrency 保存負向檢查;candidate-checked 保存四步通過的發布包與清單。