---
name: verify-ac
description: |
  施工告一段落、要判這次到底算不算達成時的站。跑硬化 oracle 給出會擋人的機械判定，另出不擋人的判斷報告，兩者分開。流程的第二個關卡，也是最後一個。

  某張單做到一個段落，要驗收的時候。例如「驗收一下」「這樣算完了嗎」「跑一次判定」
  「可以出貨了嗎」，或剛從 engineering 交出來。

  判 PASS 之後也在這裡寫交付紀錄，供釋出尾段讀。

  不用於：還在做、只是想跑個測試看看（那是 engineering 的量測）、
  決定下一站是哪一站（走 driving-work-to-done）。
metadata:
  version: 3.0.0
scope: universal
tools:
  - name: jq
    provision: framework
    why: selftest 讀 JSON 輸出裡的欄位
    install: mise:aqua:jqlang/jq
---

# verify-ac — 第二關：執行 oracle

輸出有兩部分，權力不一樣：**機械判定會擋**（由 exit code 承載，不需要讀者同意）；
**判斷報告不擋**（帶引用的意見，由人裁）。把兩者混在一起，關卡就會開始擋一些沒人能精確
定義的東西，然後大家學會繞過它。

## 機械判定：三件事

```bash
# 1. assertion 沒被動過。這一步同時查兩件事：校驗值與內文對得上，且內文與 git 歷史一致。
#    對不上就停——這時審查根本還沒開始，因為成功的定義變了。
bash .claude/skills/verify-ac/scripts/frozen-assertion-fence.sh verify {issue}/index.md

# 2. 手上這條量測命令是登錄過的。換過而沒帶紅過證據的命令不被承認。
bash .claude/skills/verify-ac/scripts/record-measurement-change.sh verify \
  --ledger {issue}/.spine/measurement-ledger.json \
  --assertion-id A-P1 --command '<cmd>'

# 3. 跑量測。同時看 exit code 與正向證據——exit 0 而沒有正向證據不是通過。
bash .claude/skills/verify-ac/scripts/run-hardened-oracle.sh --command '<cmd>' \
  --require-tool rg --expect-evidence '<真的量到東西的痕跡>' \
  --evidence-out {issue}/.spine/evidence/<assertion-id>.json
```

任一項不成立就是非 PASS，沒有討論空間。

**一條命令被好幾條 assertion 共用時，用 `--assertion` 分組，一趟就產出全部證據**：

```bash
bash .claude/skills/verify-ac/scripts/run-hardened-oracle.sh --command '<cmd>' \
  --assertion A-P1 --expect-evidence '<A-P1 自己的痕跡>' --evidence-out {issue}/.spine/evidence/A-P1.json \
  --assertion A-P2 --expect-evidence '<A-P2 自己的痕跡>' --evidence-out {issue}/.spine/evidence/A-P2.json
```

命令只跑一次，每一組在同一份輸出上**各自判、各自寫**——不是把同一個判定複製 N 份，那會讓
「這條 assertion 真的被檢查過」變成假的。一組沒有自己的輸出路徑、或兩組指到同一個檔案，它會停。

`run-hardened-oracle.sh` 會先探工具能力再釘住、要求命令產出證明自己量到東西的輸出、
並原樣保留 stderr 與 exit code。

**釘住的意思是那條命令跑在一組固定的系統目錄上，所以沒有用 `--require-tool` 宣告的工具，
在它底下就是不存在。** 那不是理論上的：2026-08-30 有兩支 selftest 用 `rg`，在一般 shell
底下全綠，在這條命令底下直接 command not found。**而它紅的樣子跟「斷言真的沒過」離場碼
相同**——分得開它們的只有輸出裡那一行說著缺了什麼。這是因為工具會說謊：PATH 上較早的 shim、靜默跳過的測試、
被吞成 generic timeout 的錯誤，三者都能讓一個空的執行看起來像綠的。

### 命令與探針都不准把位置抄下來

**一張單住在哪一格是它的狀態的投影，會被重算。** 所以登錄下來的命令、以及探針解 repo 根
的寫法，兩邊都不准把「現在在哪」寫成字面值——寫下去的那一刻它就是一個會在下一次重算之後
失效的指標，而失效的樣子是「開不到檔」，讀起來像交付壞了。

**命令這一邊，執行當下才問位置**：

```bash
bash "$(bash .claude/skills/driving-work-to-done/scripts/spine-loop-state.sh find <單的目錄名>)/probes/probe.sh"
```

**`--evidence-out` 也一樣。** 記一輪或換站之後，單可能已經被搬到另一格；跑 oracle 之前重問一次
`find`，用舊路徑會在原地造出一個空殼，接著每一條都量不到。

後面不要接 `| tail -1`。`find` 命中不是剛好一個的時候會回非 0（3 ＝ 多於一個，4 ＝ 一個
都沒有），而那個離場碼會被 `tail` 吃掉，於是兩行路徑被當成一條用。`record-measurement-change.sh`
在登錄的時候會擋下把單自己的目錄名連著斜線抄進去的命令，三種寫法都算：展開後的絕對路徑、
以 `$HOME` 開頭的、相對於另一棵樹的。

**探針這一邊，起點是執行者給的工作目錄**，那幾行的權威版本在
`.claude/skills/verify-ac/scripts/lib/probe-root.sh`——**抄進探針裡，不要 source**：探針
要先有根才找得到那個檔案。

```bash
probe_repo_root "${BASH_SOURCE[0]}"   # 先問 $PWD，走不到才退回探針自己的位置
```

`$PWD` 就是 `run-hardened-oracle.sh --cwd` 指名的那棵樹。**不要從 `${BASH_SOURCE[0]}` 開始
走**：探針住在 `issues/`，而 `issues/` 巢在主 checkout 底下，所以那條路一定先撞到主
checkout——那一輪的程式碼在別的工作樹的時候，它就安靜地量了另一棵。**也不要數固定層數**：
終局那幾格底下多一層日期資料夾，單一搬進去 `../../../..` 就少一層。

## 現在過了幾條

上面那三件事是一條 assertion 一條 assertion 做的。要問「這張單十三條裡現在過了幾條」，跑這一支：

```bash
bash .claude/skills/verify-ac/scripts/report-assertions.sh --issue {issue}
bash .claude/skills/verify-ac/scripts/report-assertions.sh --issue {issue} --rerun
```

**它唯讀，而且不宣稱任何事。** 它不寫檔案，也不留下任何下游拿得去當證據的東西——判「這張
單能不能出貨」的仍然只有交付紀錄。有兩個地方能宣稱 PASS 的話，它們遲早會給出不同的答案。

它印三種判定，而且三種都印：**過、沒過、量不到**。量不到不是通過的溫和版本，它是第三種
——一個安靜的第三態，下一次就會被當成查過了。它做到第幾層也印（見下面那張表），因為一份
沒說自己做到第幾層的報告，讀起來永遠像做滿了。

逐條判定那段程式住在 `scripts/lib/assertion_verdicts.py`，報告與交付讀的是同一份。抄成
兩份的話，「報告說過了」與「交付說不行」會同時是對的，而沒有人有辦法說出哪一份錯。

## 舊層還撐著沒有

問的是：這張單走完全程，有沒有哪一步非得靠主流程要取代的那套東西（task.md、
completion-gate marker、auto-pass ledger、verify report）才走得完。

**不用自己跑，交付紀錄那一步會跑。** `record-delivery-intent.sh` 會枚舉這張單逼出了哪些
檔案、把清單餵進檢查，非 0 就不寫紀錄。想先看的話：

```bash
bash .claude/skills/verify-ac/scripts/enumerate-spine-inventory.sh --issue {issue}
bash .claude/skills/verify-ac/scripts/check-spine-legacy-layers.sh --inventory {issue}/.spine/inventory.json
```

清單用枚舉的，不是手寫的——手寫的清單由寫的人決定漏掉什麼，然後檢查就在那個漏掉的地方
變綠。它從 git diff 與 `.spine/` 現況兩處讀，兩處都不能被說服。

**「逼出幾個檔案」這個數字只印出來，不判定。** 那三個檔案是主流程自己寫的，數量恆定，對一個
常數設門檻只會是儀式。這件事 2026-08-03 才被拆掉：門檻設在 2、實際恆為 3，於是它對每一張
真單都是紅的——而因為沒有人呼叫它，紅了幾個月沒有人知道。

## 正負兩表都要驗

只有反例的驗證，一個永遠回 FAIL 的審查端也會全綠。正例要能證明**真達標的交付確實被判
PASS**，反例要能證明**被注入問題的交付確實被擋**。兩向都跑。

## 驗證強度分三層

不可以一律降到最弱那層。「一律靜態檢視」看起來省事，實際上是把可以被證偽的東西降級成不能
被證偽的東西。

| 強度 | 適用對象 | 方法 |
|---|---|---|
| 可注入 | 會被機械執行的部分 | 刻意構造情境，觀察結果 |
| dogfood 觀察 | loop 行為 | 跑真工作記錄，樣本薄就如實標示 |
| 靜態檢視 | 只能是散文的部分 | 讀指定段落，判斷條件是否成立 |

### 一條實例：某個輸出的消費者，要量那個輸出，不量它的替身

**「這個值換了來源之後，每一個讀它的地方拿到的東西都沒變」是第一列，不是第三列。** 它會被
機械執行，所以它的量法是真的去取那個輸出——渲染出來的那一塊、API 回應的那一格——不是一支
單元測試說它組出來的字串對。單元測試量的是組裝那一步，而消費者拿到的是組裝、傳遞、快取、
渲染全部跑完之後的東西；那幾層之中有一層換了來源，測試照樣綠。

**要比就說出比的是哪一段。** 兩次部署之間整份回應幾乎不會逐位元組相同（時間戳、cache
busting、build id），所以這一條要指名的是那個消費者印出來的那一塊文字或那一個欄位，不是
整份 response。

**跨 repo 的單還要說出哪一邊算「改動後」。** 一個值流經好幾個服務的時候，「新舊兩個 build」
不是一台機器——哪一台裝的是這一輪的改動、哪幾台維持原樣，由那張單自己說出來，判定這一站
不替它挑。

**換一個情境再驗之前，先確認量的還是同一個東西。** 兩種常見的替身：

- **換視窗或換平台。** 同一個網址、同一個操作，開出來的可以是另一支元件。記下開出來的那個
  元件，跟改動的元件對一次；對不上，那一格的綠是「沒壞過」，不是「修好了」。2026-09-16 的
  標本：一個修正在桌機、手機各驗兩格，四格全綠；手機那兩格開的是另一支從來沒有這個 bug 的
  元件。
- **驗一個症狀好了沒。** 打立案時量的那幾條原始輸入，不自己挑一條看起來同類的；先問那顆
  commit 上線了沒（`git branch -r --contains <sha>`、`git tag --contains <sha>`），併進開發
  分支不等於上線。否定一份報告之前也一樣：先確認量的是它講的那個機制。2026-09-17 的標本：
  驗收打的網址走的是同一支函式的另一個分支，結果寫成「症狀已消失」；重打立案那批網址，症狀
  逐字相同，而那顆 commit 根本還沒上線。
- **在共用資料的環境判「是不是我的改動造成的」。** 同一台機器上的前後差，不等於我造成的：
  共用的資料庫與上游在那段時間裡也會變。對照組是同一時刻沒被重新部署的另一台，而且只看我的
  改動碰得到的那一格。2026-09-16 的標本：兩台測試機的總條數同時都在變，分不出結論；改看
  只有這次改動會動到的那一格，同一分鐘重部過的那台是 1、沒動過的那台是 0。

### 斷言的每一個子句，都要對到一段輸出

**一條斷言寫了幾件要成立的事，量測輸出就要有幾段對得上。**「什麼時候觸發、算出什麼」是兩件
事。只量觸發次數的命令，對後半句什麼都沒說，而它的離場碼照樣是 0。機械判定只認離場碼與
標記，分不出一條命令量的是整條斷言還是半條。

所以判定這一站逐條對一次：把斷言拆成它要成立的子句，每一個子句指出輸出裡對得上的那一行。
**對不到的子句，那一條不算成立。** 判斷報告指名是哪一個子句沒有量到；機械判定是綠的也一樣，
因為綠的離場碼證的是命令量到的那一半，不是整條斷言。處置跟下面〈判斷報告〉那條相同：回
`engineering` 補量，不把斷言讀窄去遷就輸出。

這一步不做成關卡。子句是自然語言，拿它去比輸出，機械化只會做出一道對字面的假綠。它靠判定
的人讀。

2026-09-30 的標本：一條斷言寫「三條計價路徑什麼時候觸發、算出什麼，與改動前逐項相同」。量測
輸出每一行是「觸發幾次、有沒有觸發」，五行全 OK，整張單判定全綠。後半句沒有任何一行在量。
上線之後，換選項時結帳金額算成 0、無法下單，在線上約十九天才被回報。補量不保證當時抓得到
——那個壞法要換選項才出現——但沒量的那半句，判定不該替它說 OK。

## 判斷報告

處理只能是散文、無法被執行的部分。結論一律引用具體 `path:line`——沒有引用的 assertion 在報告裡
沒有份量。

報告不產生 PASS / FAIL，它產生的是「我讀了這段，依據是這幾行，我的判斷是這樣」，交給人決定。

**不得為了遷就交付的東西而放寬 assertion。** 證據構不到 assertion 時，那是實作沒走到，不是 assertion 寫錯了
——assertion 承載的是人簽過的意圖，而在判定這一站把它調鬆，等於自己改掉自己要對照的那把尺。
真的是 assertion 本身錯了，處理是停 `assertion_wrong` 回第一關重簽，那會留下一個有人看得到的
commit；就地放寬不會。

dogfood 觀察的樣本通常很薄，而且觀察者往往同時是被觀察者。這不使結論無效，但它使結論不是
統計證據。如實標示樣本數與觀察者身分，不要把「跑過一次」寫成「已驗證」。

## 施工方自己挑的 oracle，漏掉了什麼

上面那三層擋得住偽造的證據，**擋不住一條從第一天就量錯東西的命令**——它一直是綠的，因為
沒有人拿一份壞掉的實作去試它會不會紅。挑那條命令的人跟寫實作的是同一個，而他挑的時候
已經知道自己寫了什麼。

所以多一條路：**讓一個看不到你的對話的 agent，從同一份 assertion 自己推一組探針，跑完再
跟你的比。**

```bash
# 1. 它只拿得到一份去掉施工痕跡的單（只有 plan 與 assertion）。那份放在單以外的地方，
#    名字也不帶單號——放在單底下的話，它離你的量測腳本只有一個 `..`。
bash .claude/skills/verify-ac/scripts/build-independent-oracle-prompt.sh \
  --issue {issue} --tree <要量的 checkout> --phase 1
# 把印出來的那份指示原樣交給一個 agent，等它寫出 {issue}/.spine/independent/phase1.json。
# 或加 --agent-cmd '<啟動命令>'，由這支在那棵樹底下派：指示接在最後一個參數，
# 例如 'claude -p --permission-mode acceptEdits'、Codex 是 'codex exec'。

# 2. phase1.json 在了，第二份指示才產得出來。這時才給它你的量測登錄。
bash .claude/skills/verify-ac/scripts/build-independent-oracle-prompt.sh \
  --issue {issue} --tree <同一棵> --phase 2

# 3. 收成報告：三類各幾條、量不到的有哪些、這一趟花了多少
bash .claude/skills/verify-ac/scripts/record-independent-oracle.sh \
  --issue {issue} [--cost-tokens N] [--cost-seconds N]
```

**出貨之前兩個階段都要跑完，不只第一階段。** 手寫的測資繞過了取值的程式碼這一類，是第二階段
拿到你的量測登錄之後才比得出來的。

**順序由檔案系統擋住，不由叮嚀擋住。** 第一階段那份指示裡沒有你的命令，而且產得出第二份
的前提是第一階段的結果已經落地。「先自己推」因此不是一句請求。

**那份指示沒有第三個參數，這是刻意的。** 你交得出去的只有「單在哪」「樹在哪」。一個
「可選的補充說明」看起來無害，而它正好會把這一趟唯一的根據送掉——那個 agent 的價值完全
來自它沒看過你的說法。`--agent-cmd` 不算：它換的是誰去跑，指示一個字都不變。

**它產出的是意見，不是判定。** `record-independent-oracle.sh` 的離場碼說的是「收不收得成」，
不是「過了沒」；判「這張單能不能出貨」的仍然只有交付紀錄。把一個 LLM 的判斷接到會擋人的
路徑上是往下走——EACL 2026 量到，一行「this is correct」的註解就能讓 LLM-as-judge 的判對率
動 34 個百分點。

**三類都要說出來，空的也要說**：只有你量到的、只有它量到的、兩邊都量到的。`independent_only`
那一類是這一趟的產出；它是空的**也是一個結果**，照實記著——那是「這一層值不值得」要被重估
的證據，不是一個要被補滿的空格。

**不要把這一趟寫成 held-out 驗證，也不要寫成獨立稽核。** held-out 的定義是那份測資被驗的人
優化不到，而這裡：派工的是你、跑的是同一棵樹、探針由 LLM 從同一份 assertion 推，三件都不
滿足；信任域也沒有換（見〈這一層擋得住什麼、擋不住什麼〉）。它拿掉的那一樣東西是**敘事
污染**——那個 agent 看不到你的自我背書。能宣稱的就是這樣，不多。

**它碰得到的東西要有上界，而那個上界只寫在指示裡。** 2026-09-21 實測：這台機器上一個背景
sub-agent 手上瀏覽器、JIRA、Slack 送訊息全部叫得到，憑證檔讀得到，工作目錄預設是主
checkout。所以指示裡逐條寫明不得送 Slack、不得寫 JIRA、不得送出 review、不得推 ref、不得
改動那棵樹。**這是一份會被自願遵守的界線，不是一道閘**——這一句要先講，一份把自己說成
安全網的說明會讓下一個人不去問「那誰在擋」。真正在擋的仍然是那棵樹上的 git hook 與看
diff 的人。

## 讓別人看得到：報告與清單

判定的價值在於**別人能核對**，而上面那一支只印到終端——一份沒有人看得到的判定，跟沒有判定
的差別只有磁碟空間。

```bash
bash .claude/skills/verify-ac/scripts/render-evidence-report.sh --issue {issue}
bash .claude/skills/verify-ac/scripts/render-evidence-report.sh --issue {issue} --publish
```

**量完之後又落了 commit 的時候。** 證據綁在它量到的那個 head 上，而交付之前常常還會有東西
落下去——單自己的散文、`.spine/` 底下的紀錄。那時候直接跑會逐條判「量在 X，要交付 Y」，
報告尾巴還會多一行 BLOCKER。**那不是判定壞掉，是沒有人說過那段差異碰了什麼：**

```bash
bash .claude/skills/verify-ac/scripts/render-evidence-report.sh --issue {issue} \
  --head <要交付的 head> --delta-allows issues/ --delta-allows {issue}/
```

`--delta-allows` 可以給很多次，每次一條路徑前綴。**它驗證你的主張，不代你宣告**：那段差異
真的只碰了指名的路徑才放行，碰到別的就逐條判 FAIL 並指名是哪幾個檔案，兩個 commit 沒有樹
看得到就判 unmeasurable——問不到不放行。所以它擴不出「把整棵樹指名進去就全過」這種用法，
指名整棵樹的人自己會在報告上看到那段差異碰了什麼。

**沒有 `--head` 就沒有那段差異可言**，所以只給 `--delta-allows` 會被拒絕。

它產兩個檔案，因為讀的人有兩種：`report.md` 給人看，`manifest.json` 給機器讀（逐條的判定、
綁的 head、量測命令、要跟著一起送出去的檔案）。要一起送圖之類的東西，把它們放進
`{issue}/.spine/attachments/`——一個目錄的存在與否就是答案，不需要多一個設定。

**它唯讀，而且不判定成敗。** 讀的是已經被 oracle 判定過的東西，只是把它排版；不寫輪次
狀態、不寫交付紀錄、不碰證據，也不成為第二條可以宣稱 PASS 的路徑。

**有東西沒過照樣產得出來。** 最想看報告的那一刻，正是有東西沒過的那一刻——所以離場碼說的
是「產不產得出來」，不是「過了沒」。

### 送去哪，這裡不知道

`--publish` 掃所有 skill 找一行宣告，找到就把那兩個檔案原樣交給它，由它決定怎麼送：

```
<!-- {任意前綴}-EVIDENCE-PUBLISH-{命名空間}: {命令} -->
```

**這一層不認得任何一個目的地**，跟它不認得釋出尾段叫什麼名字是同一個理由：一旦這裡指名某
一種外部系統，這四支 skill 就搬不到沒有那個系統的地方了。

沒有人宣告那個命名空間時它**說出來並指名是哪一個**，報告留在本機，離場碼 4——那不是壞掉，
是一個要人回答的問題。不沿用別的命名空間的宣告，也不猜一個目的地。

**發佈不是關卡。** 送不出去要吵、要留下紀錄，但一條 assertion 的判定不會因此改變：判定由 oracle
決定，發佈是那個判定的投影。一次逾時不得把一份綠的交付變成紅的。

## 判 PASS 之後：交出去

判定通過不等於交付。釋出尾段要的是一份它讀得動的紀錄，不是一句「verify-ac 說 PASS」：

```bash
bash .claude/skills/verify-ac/scripts/record-delivery-intent.sh \
  --issue {issue} \
  --summary '<一句話，說出交付了什麼>'
```

**版本不在這裡，連欄位都沒有。** 有些專案的交付是一張單與一次部署，沒有 patch / minor /
major 可以宣告。留一個「可以不填」的格子仍然是在教一套詞彙——只下載了這一支的人不會知道
那個格子是給誰看的，而它只對某一條釋出尾段有意義。

所以版號整件事住在釋出尾段，宣告源是那條尾段本來就會讀的東西。這一站不宣告、不轉述、
不留欄位。

它會先重驗 fence 才寫。assertion 被動過的單記錄不了交付意向——對著一份沒人簽過的成功定義
出貨，比不出貨糟。

**紀錄裡有一格 `self_check`，指向開 PR 前的自檢紀錄，不判定。** `engineering` 要求開 PR
之前跑 `check-your-own-work`，紀錄寫成單上以行首 `check-your-own-work 九問` 開頭的一段。
這一格列出那幾段的行號，輸出也印 `SELF-CHECK:` 一行；找不到就寫「沒跑」，pack 為 `none`
的單寫不適用，讀不到 pack 或讀壞了就說出讀不到什麼。**每一種情況交付紀錄都照寫、exit code
都一樣**——它讓「沒跑」看得見，不擋人。

**同一個待遇的還有一格 `independent`，指向上面那一趟盲測。** 單上有
`{issue}/.spine/independent/phase1.json` 就記成跑了；`record-independent-oracle.sh` 收成了報告，再記
`independent_only` 幾條。沒有就寫「沒跑」，輸出印 `INDEPENDENT:` 一行。它跟 `self_check`
一樣不判定、不改 exit code。

接著它逐條檢查 fence 宣告的每個 assertion ID。證據的可信度分三層，**交付這條路三層全做**：

| 層 | 問的是 | 少了它，什麼東西過得了 |
|---|---|---|
| 檔案自洽 | 有沒有 `{issue}/.spine/evidence/{ID}.json`、`verdict` 是不是 `PASS`、`producer` 是不是 `run-hardened-oracle.sh`、每一條是不是量在同一棵樹的同一個 commit | 什麼證據都不用，「verify-ac 說 PASS」一路靠散文帶著走 |
| 登錄相符 | 證據記的那條命令，是不是這條 assertion 在量測登錄裡簽過的那一條 | 一份手寫的證據可以自己指名一條「一定會過」的命令 |
| 重跑一次 | 拿那條命令現在再跑一次，還綠不綠 | 前兩層讀的都是檔案，而一個檔案的內容是誰寫的它自己說了算 |

同一條命令通常被好幾條 assertion 共用，所以重跑會去重。**去重的鍵是三樣**：命令、在哪棵樹跑、
要哪些工具。八條 assertion 共用一條命令、跑在同一棵樹、要同一組工具，那就是同一個鍵——跑一趟。

**正負向樣式不在鍵裡，這是刻意的。** 它們決定的是判定，不是執行：那一趟跑完之後，八條各自拿
自己的樣式去判同一份輸出，各自寫自己的證據檔。少了那一步，第二條 assertion 會拿到第一條的答案，
而它自己的樣式從來沒有被檢查過——**一條沒被量到的 assertion 看起來就跟過了一樣**。所以去重放寬
的是執行，不是判定。

要估時間就照命令數（加上樹與工具）去數。**重跑那一步在跑第一趟之前會把趟數印出來**，不用
自己數。

**以前不是這樣。** 樣式曾經也在鍵裡，於是 2026-08-27 有一張單 21 條 assertion 共用一條命令、
各帶各的樣式，跑了 21 趟同一件事——差出來的形狀是一個兩分鐘的逾時。那個行為在 DP-630
（`524aaacb`）被改掉了，同一張單現在跑 1 趟。這個例子留著，是因為它解釋了為什麼會有人相信
另一個版本。

**登錄檔在，就每一條 assertion 都要在裡面。** 「這條沒登錄過」不是豁免，那正好是一份手寫證據會長
的樣子：它指名一條沒有人簽過的命令。

### 這一層擋得住什麼、擋不住什麼

**擋得住**：手寫一份自洽的 JSON。`producer` 抄對了，命令沒登錄過；命令抄成登錄過的那一條，
重跑就是紅的。

**擋不住**，逐條說出來：

- **一個能在同一棵樹上執行任意命令的施工端。** 它可以把量測命令本身換成一條永遠會過的
  命令，然後三層全綠。擋這件事的不是這裡，是登錄那一層要求換命令必須帶「實作之前紅過」的
  證據，而登錄與那份證據都在 git 歷史裡——偽造得留下一個有人看得到的 commit。
- **一條從第一天就量不到目標的命令。** 它一直是綠的，因為沒有人拿一份壞掉的實作去試它會不會紅。擋它的是 engineering
  登錄 baseline 時的紀律，不是這裡。
- **換掉那條命令跑到的東西，而不動命令字串。** 「登錄相符」比對的是**命令字串的 hash**，
  不是那條命令會執行到的內容——命令指向一支腳本，而那支腳本的內容換掉了，這一層看不見。
  擋這件事的是那支腳本本身也在 git 裡：改動會出現在 diff 裡，而看 diff 的人是那道網。
  2026-10-27 兩例：一張單交付被擋之後，判定腳本裡那一段被改到過關；另一張單壓版之後，探針
  改成排除版號檔。兩例命令字串都沒動，登錄看不到，都是寫在單上、出現在 diff 裡才被看見。
- **散文那一半。** 判斷報告不歸這三層管，它本來就不擋人。

所以這三層不是證明。它做的是把偽造的成本從「寫一個 JSON 檔」提高到「改一條登錄過的命令，
而那個改動會出現在 diff 裡」。**不要把它說成擋得住所有偽造**——一個宣稱自己滴水不漏的檢查，
會讓下一個人不再看 diff，而那正是它真正靠著的東西。

**證據量在哪棵樹，用 `--cwd` 指名，不要靠命令自己 `cd` 進去。** 這兩件事看起來一樣，
結果不一樣：`run-hardened-oracle.sh` 的 `head_sha` 與量測所在都從**它讓命令跑起來的那個
目錄**取，而那個目錄由 `--cwd` 決定。命令字串裡自己 `cd` 進產品 repo 的話，那個目錄仍然是
你站的地方——量測本身跑對了，記下來的 head 卻取自另一棵樹。

**那份紀錄看起來完全正常**，而它綁的 commit 跟這張單的產出無關：那棵樹上任何人壓一次版，
同一批證據就分屬兩個 head，交付紀錄因此寫不成，要整批重量。所以：

```bash
bash .claude/skills/verify-ac/scripts/run-hardened-oracle.sh --command '<cmd>' \
  --cwd <這張單的改動真的落下去的那棵樹> ...
```

**`--cwd` 一換，命令裡的相對路徑就跟著換基準。** 量測腳本常常住在單裡，而單住在
`issues/`——用一條相對框架 repo 根的路徑去叫它，`--cwd` 指到產品的樹之後那條路徑就不存在了
（實測 `exit 127`）。**改成絕對路徑不是出路**：登錄那一步會擋下抄了這台機器路徑的命令
（`POLARIS_MEASUREMENT_COMMAND_CARRIES_A_PATH`）。

那道拒絕自己就寫著出路——**單的目錄問 `spine-loop-state.sh find`**。它從當下位置往上找
`issues/`，所以在產品的工作樹裡也答得出來：

```bash
--cwd <那棵樹> \
--command 'bash "$(... spine-loop-state.sh find <單名> | tail -1)/verify/verify.sh" A-P1'
```

三種都不適用時才用 `--exempt-path` 與 `--exempt-why` 具名寫進登錄。

**逐條判定那一層**會把證據記的樹跟這張單宣告的落腳處比一次，落在宣告外的樹是一條
blocker。它在那一層而不在寫紀錄那一步，是因為報告走的是同一個判定——一條只寫在交付
腳本裡的規矩，看報告的人讀不到。

**一張單宣告了不只一棵樹是被支援的形狀**，不是歧義：每一棵各自記一個 head，交付紀錄多
一份 `heads`（單樹的紀錄不長這個欄位，所以既有的紀錄一份都不用動）。**宣告了卻一條證據
都沒有的那幾棵樹會被指名說出來**——把 N 棵樹的量測全部釘在同一棵上，紀錄寫得出來而它
對另外幾棵零綁定，那條路可能仍然是對的選擇，但它不能安靜。

宣告問不到的時候它說出來並走回嚴格的那一條（證據來自不只一棵樹就擋）——一個問不到的
宣告不得比一個答得出來的宣告寬。

**交付的 head 從證據來，不從你站的地方來。** 一張單住在 `issues/`、程式碼落在某個產品
repo 是常態，兩棵樹的 head 不一樣；問當下的位置只有在兩者剛好重合時才對，而它答錯的時候
長得跟答對一模一樣。量完之後那棵樹又有新 commit 落下去的話它會擋——那一條是回頭問證據
自己記下的那棵樹現在在哪，不是問你在哪。真的要指定就用 `--head`。

`destination` 決定去哪：`workspace` 留在本地，`template` 才進 Polaris template repo。那是
第一關由人宣告的，這裡只讀不改。

紀錄寫完，這一站就結束了。之後怎麼出貨**不在這條流程裡**——釋出尾段是每個專案自己的事，
它讀這份紀錄，而這一站不需要知道它叫什麼名字。這是刻意的：一旦這裡指名某支釋出腳本，
這四支 skill 就搬不到沒有那支腳本的地方了。

**把判定結果帶回 `driving-work-to-done`**——PASS 之後要不要
推站別、非 PASS 要回哪一站、原因是實作沒到還是 assertion 本身錯了該停哪一種，都在那裡回答，
不在這裡。這一站產出的是判定，不是下一步。

<!-- PROSE-EXTERNAL-PATHS: report.md — 跑起來才長出來的東西，落在那張單的 .spine/report/ 底下（單住在別的 repo） -->
<!-- PROSE-EXTERNAL-PATHS: manifest.json — 同上，跟 report.md 同一次產出 -->
<!-- PROSE-EXTERNAL-PATHS: docs-manager/ — 動手對象：那是 specs 站台自己的 repo，這支 skill 往它寫東西、讀它的結構，不是我們抄一份放著的知識 -->
