系列:「從單一 agent 到多 agent 集群,再到接進我的生活」;不設天數,第 12 篇下半
上篇說完十六條紅怎麼分成四種、怎麼一種一種綠掉,留一條 route ratchet 沒修,等操作者裁
memory 的產品語意。這篇接著說:那條紅底下藏著另一件事——lib 從來沒有 Fresh 過;白天的
Gatekeeper 是什麼樣子;全量最後真的收工了,唯一一條紅正是留案的那條;還有一道差點連自己
都分不出放行跟繞過的閘。

這一節是今天最大的一件事,而它不在早上的清單裡。
紅證 targeted 09:31 起跑,只有 5 支測試 binary,lib 原始碼自 414db8c4 起沒動。09:40 它的cargo test --no-run 跑了 9 分鐘還在編;ps 看到 rustc 正在 --crate-name spectyn_mesh——
它在重編 lib。去讀 core/build.rs(8ef2455b 之前的版本,37 行):
let git_dirty = run("git", &["status", "--porcelain"]) // :20-22
.map(|s| if s.is_empty() { "" } else { "+" })
println!("cargo:rustc-env=SPECTYN_GIT_HASH={}{}", git_hash, git_dirty); // :31
// Re-run when HEAD moves (covers commits + checkouts).
println!("cargo:rerun-if-changed=.git/HEAD"); // :35
println!("cargo:rerun-if-changed=.git/index"); // :36
lib 把 SPECTYN_GIT_HASH 烤進 core_sha()(core/src/lib.rs:216)。09:43 的假設是:它盯著.git/index,而 git add、git status、tree 從 clean 翻到 dirty 都會改 index;每一次 index
一動,build.rs 重跑、env 變、lib 重編、202 支重 link、Gatekeeper 重付。這已經夠糟——而且看起來
解釋了今天早上為什麼要把 commit 集中一次做。派一個 worker 起草修法、一個駁斥者推翻。
worker 回來說:比那更糟。那兩條路徑是相對 core/ 的,build script 的 cwd 是 core/,而core/.git 不存在——repo 的 .git 在上一層。cargo 對一條找不到的 rerun-if-changed 路徑的
處理是:當它 stale。所以不是「index 一動就重編」,是每一次 cargo 都重編。
先紅證,再套修法。09:57 tree 乾淨、HEAD 6ddf4ae4,CARGO_LOG=cargo::core::compiler::fingerprint=info,cargo build -v 連跑兩次,中間不碰任何東西(f0-step0.log):
core/.git exists: NO (第 2 行)
fingerprint …/run-build-script-build-script-build.json: "paths":[".git/HEAD",".git/index"] (第 3-5 行,三份)
=== BUILD 1 (09:57:04) ===
stale: missing ".../spectyn-mesh-private/core/.git/HEAD" (第 7 行)
Dirty spectyn-mesh v0.6.0 (…/core): the file `.git/HEAD` is missing (第 13 行)
Running …/build-script-build (第 15 行)
Finished `dev` profile in 1m 05s (第 16 行)
=== BUILD 2 (09:58:10) — NOTHING changed ===
Dirty spectyn-mesh v0.6.0 (…/core): the file `.git/HEAD` is missing (第 25 行)
Running …/build-script-build (第 27 行)
Finished `dev` profile in 43.47s (第 28 行)
BUILD 2 什麼都沒改:同一行 Dirty,build script 又跑,lib 又編了 43 秒。三份 fingerprint 檔
存的都是那兩條相對路徑。假設證實,而且是最糟的那個版本:這個 crate 的 lib 從這條 build.rs
進 tree 那天起,在這台機器上一次都沒有 Fresh 過。 修之後全量的 12m47s、紅證 targeted 的
16m46s、Day 8 到 Day 11 每一輪 Gatekeeper 的全額——每一輪都在重編一個沒有變的 lib,然後重
link、重審 202 支。
day12-f0-buildrs.diff,+76/−8,現在是 f3fcb84f 的 core/build.rs。三條規則,檔頭自己寫著
(build.rs:15-24):
.git/index。 git add、git status(它會刷新 index)、每一次 clean↔dirty:16-17)。git rev-parse --git-path 解析(git_path(),:48-58)。git 自己會算這個../.git/HEAD,.wt/* 那種 linked worktree 回.git/worktrees/<name>/ 底下的絕對路徑(:18-23)。p.is_file().then_some(p),:57;規則 :24)。找不到的路徑,一條都不印。盯的是四個檔(:88-108):HEAD、HEAD 指到的那個 ref 檔(ref: refs/heads/x →refs/heads/x)、packed-refs、logs/HEAD。另外先印一條 rerun-if-changed=build.rs 當錨
(:67-71)——不然在沒有 git 的環境四個都 None,cargo 會退回「package 目錄任何東西變了就重跑」。
dirty 的 + 拿掉了(:85 現在只印 SPECTYN_GIT_HASH={git_hash})。rustc-env 是整個 package
共用的,給不了 bin 專用;而且不再隨 tree 改動重跑之後,編譯期的 dirty 旗標只會凍結在上次 HEAD
移動那一刻的狀態,會說謊(:26-29)。
修法套進 working tree、不 commit(探針要量 git add,commit 會動 HEAD),10:02:04 起
(f0-step1.log,HEAD 8ef2455b):
| 做了什麼 | 預期 | 結果 | log 行 | |
|---|---|---|---|---|
| B1 | build.rs 變了 | Dirty 一次 | Dirty … the file build.rs has changed,build script 跑,14m 50s |
5-8 |
| B2 | 什麼都沒改 | Fresh | Fresh spectyn-mesh v0.6.0,0.65 s |
11-12 |
| B3 | touch 一個 docs 檔 + git add |
Fresh | Fresh,0.30 s | 16-17 |
| B4 | git status --porcelain |
Fresh | Fresh,0.30 s | 21-22 |
| B5 | core/src/lib.rs 加一行註解 |
Dirty,但 build script 不重跑 | Dirty … src/lib.rs has changed,沒有 Running …/build-script-build 那行,45.49 s |
26-28 |
| B6 | 還原 lib.rs | Dirty | Dirty,36.31 s | 32-34 |
| — | spectyn --version |
沒有 + |
spectyn 0.6.0 (8ef2455b46, macos-aarch64, built 2026-09-05)——tree 有 6 個檔髒著 |
38 |
B2 那個 Fresh 是這台機器上第一次。修之前同一個情境(Step 0 的 BUILD 2)是 Dirty + 43 秒。
B1 的 14 分 50 秒要解釋。build.rs 本身改了,lib 重編一次是預期的;但 B5、B6 同樣重編 lib 只要
36 到 45 秒。10:16 去看:rustc --crate-name spectyn_mesh 跑了 12 分鐘,累積 CPU 5.5 秒不再
增加,四條 thread 全 S;lsof 看到它載入了 21 個 proc-macro dylib;syspolicyd 65% CPU,log show 說它在做 GK library assessment,每一筆去問 Apple 要公證票——ticket not available、Error checking with notarization daemon: 3——5 分鐘 9 筆(live 看的,沒重量)。
Gatekeeper 換了個地方發作:不是執行剛 link 出來的 binary,是 rustc 載入剛編出來的 proc-macro
dylib,一顆一顆審、一顆一顆問 Apple。約 13 分鐘是它(估:14m50s 減掉 lib 本身 40 秒上下的重編)。
順帶一提:09:31 那輪紅證 targeted 編了 16m46s、lib 原始碼一個字沒動,多出的那 15 分鐘形狀跟
B1 一模一樣——那輪撞沒撞到這個 dylib 審核,沒去查,寫在這裡。
兩個駁斥者都沒推翻,各補了一件沒查到的:
cargo::rerun-if-changed=foo where foo is a file that doesn't existrerun-if-changed in thisdoc.rust-lang.org/cargo/faq.html,一字不差。這條 build.rs 在文件寫明的坑裡git log 可以查,今天沒查。.git/HEAD 是一個符號 ref,commit 不會改它。 它的內容是 ref: refs/heads/dev/hive-2026-08-14;.git/HEAD 的 mtime 是 09-03 20:13:38,之後有 22 個 commit;refs/heads/dev/hive-2026-08-14 跟 logs/HEAD 的 mtime 是當天 14:35:37(最近一個 commit)。rerun-if-changed=.git/HEAD 就算路徑對了,註解裡寫的「covers commits」也從來logs/HEAD,就是為了這件事(build.rs:90-92、:105-108)。build.rs:5-8)。所以「hash 沒變就不重編」這種期待從來早上這輪工作全篇的前提是「lib 一改才重付」。前提錯了。不是「lib 一改」,是每一次。紅證
targeted lib 一個字沒動,照樣重編了 16 分鐘。
那排程還對嗎?對,但理由換了。Gatekeeper 收稅的單位是「新 link 出來、第一次被執行的 binary」:
5 支約 4 分鐘,202 支 11 小時。targeted 只 link 5 支,所以便宜——跟 lib 有沒有重編無關。
lib 重編是另一筆稅(1 到 17 分鐘,今天量到的範圍),F0 修好之後這筆只在 HEAD 移動或 core/src
真的變了才付。
修好之後還有一筆:每一個 commit 都會動 ref 檔,build.rs 重跑,hash 變,lib 重編,202 支
重 link——包括只改 docs 的 commit。這是設計上的(hash 要對),不是 bug。10:24 的 lib commitf3fcb84f 之後第一次 cargo(targeted)就付了這筆(targeted-fix.log:4 Compiling spectyn-mesh);
付完之後 14:31 的 Phase D 是 Finished 0.70s、14:34 的全量是 Finished 0.80s。對排程的意思是:
全量起跑之前把 commit 全部做完,build 階段不 commit;git add、git status、改 docs 檔,
現在都可以了(B3、B4)。

Day 11 收工時寫:「修之後的全量在寫這一段的時候還在跑(20:13 起跑,201 支 binary 重 link,
Gatekeeper 估 80 分鐘)。」
實際量到的:
| 時間 | 狀態 | 證據 |
|---|---|---|
| 20:13:44 | 起跑,HEAD 414db8c4 |
full22-after.log:1 |
| 20:26 | 編完,12m 47s;之後零行輸出 | full22-after.log:275 |
| 06:27 | 20 個 --list 子行程在等,frontier p8_*,最老的等了 27:20 → 每支 ~80 s |
pgrep -P |
| 06:31 | 20 支的窗只跨 4:07 → 每支 ~12 s;syspolicyd 0% | 同上 |
| 07:39:34 | --list 結束、測試開跑 |
junit-full-after-20260905-0836.xml timestamp |
| 08:36:25 | Summary:3593 / 3577 / 16 / 70,EXIT=100 | full22-after.log:4297、:4315 |
估 80 分鐘,實際光 --list 就跑了 11 小時 13 分。夜裡為什麼從 25 秒一支慢到 80 秒一支、
再回到 12 秒、再到 60 秒,原因沒查。
14:38,操作者回答了那個一直沒答的裁定點(D12-0):「加了,Terminal 已經在開發者工具裡。」但
full23 是 14:34:33、在加之前起跑的。14:40 量它:--list 子行程 etime 精確每 26 秒一支
(00:05、00:35、01:01…04:03),frontier cli_namespaces,syspolicyd 40.5%,log show 3 分鐘
16 筆 assessment——豁免對這棵已經在跑的行程樹沒有生效(估:它看的是新起的行程,不是已經
在跑的)。不殺、不重起;照 26 秒一支算,202 支估 87 分鐘,比夜裡的 11 小時好得多,但仍是一筆
稅。豁免是不是真的有效,要等 full23 收工後,另外起一輪新的 cargo 才量得到——這句話今天沒有
機會兌現,下面會說為什麼。
15:00 那一輪,批評者(CRIT)標了一個 blocker:這個 5 小時的 session 視窗,照今天已經死過兩次
的死法(每次死亡前約 25 到 26 個 subagent),估計會在全量的 Summary 落地之前見底——瓶頸是
session 用量,不是 CPU,cap=8 沒有對到這個限制。同一輪 CRIT 也把之前記錯的 binary 數量更正:
是 203 支、195 支沒被 --list 過,不是這份文件之前寫的 202/194(414db8c4 之後多了一支放行側
測試的 binary,一直沒被算進去)。
--list 段全程用 Monitor 盯著,不再逐輪 poll。16:00 前後 Monitor 觸發:Starting 3595 tests across 203 binaries (70 tests skipped)——203 支確認,CRIT 的更正成立。--list 段從 14:34:33
到這裡估 ~90 分鐘,跟「白天 26 秒/支 × 195 支未審過的」的估算一致。CRIT 擔心的視窗見底,這次
沒有發生——15:00 那輪之後每一輪都刻意派 0 個新 agent,把視窗留給等 Summary,撐到了 16:24
全量真正收工。
白天(daytime,已知 Terminal.app 早已在開發者工具裡的這一整段窗口)跟夜裡的對比,現在有兩個
數字可以放在一起:夜裡 11h13m、白天約 90 分鐘,快了大約 7 倍。但這不是豁免生效的證據——
豁免是 14:38 才按下去的,而 full23 這棵行程樹在那之前就起跑了,量到「豁免沒有立刻生效」的正是
今天。白天本來就比夜裡快(Day 11 量的白天基準是 ~25 秒一支,今天 26 秒一支,幾乎一樣),這筆
7 倍的差距目前只能歸給「白天,不是夜裡」,不能歸給「加了開發者工具豁免」。豁免對一次全新起跑的--list 有沒有幫助,今天沒有測到——下一次全新的 cargo --list(而不是接續已經在跑的樹)才是
第一次真的量。
-E 篩哪一層10:24 的 targeted 寫成 cargo nextest run -E 'binary(a) | binary(b) | … | (kind(lib) & test(…))'
(targeted-fix.log:2-3),以為它跟 --test a --test b 一樣只編那幾支。10:30 去看:六個 rustc
同時在編 p22_cli_gate、fleet_live_smoke、eval_insta_snapshot 這些跟 filter 無關的 test crate。-E 的 filterset 只篩「跑哪些測試」,不篩 build——它把整個 package 的 test target 全編了,Finished 在 15m23s。要縮 build 只能用 --test <name> / --lib。
10:30 進一步寫「接著會對 202 支逐支 --list,Gatekeeper 全額」。這一半錯了。10:45 收工那行是Starting 101 tests across 8 binaries (2730 tests and 195 binaries skipped)——binary() 有
篩 list,nextest 只對 8 支列舉,約 5 分鐘。所以 -E 篩兩層(執行、列舉)、不篩一層(build)。這
15 分鐘的稅沒有白付:它把 202 支 binary 都在 f3fcb84f 之後 link 好了,只是還沒過 Gatekeeper。
規則跟著來——targeted 之後、全量結束之前,不 commit;不然 HEAD 一動,lib 重編,202 支
重 link,剛付的稅作廢。
14:34:33 全量起跑(full23.log:1,HEAD c2a59c5b,dirty_repo=3——三個 docs 檔):Finished … in 0.80s(第 281 行)。這個數字要說清楚是推論還是量到的:full23.log 從第 9 到 280
行全是編譯警告,grep -c Fresh 是 0——log 沒帶 -v,不印 Fresh。「202/203 支全部 Fresh、一支
都沒重編」是從 0.80 秒這個總時長推論出來的(比對 F0 綠證裡量到的 Fresh 就是零點幾秒這件事),
不是 log 白紙黑字寫的。
16:24:02,Monitor 觸發,full23.log 印出:
START 2026-09-05 14:34:33 HEAD=c2a59c5b core_clean=1 dirty_core=0 dirty_repo=3 (第 1 行)
Starting 3595 tests across 203 binaries (70 tests skipped) (第 284 行)
Summary [1327.625s] 3595 tests run: 3594 passed (1 leaky), 1 failed, 70 skipped (第 3899 行)
EXIT=100 (第 3902 行)
DURATION 6569s (第 3906 行)
END 2026-09-05 16:24:02 (第 3907 行)
14:34:33 到 16:24:02,DURATION 6569 秒 = 1 小時 49 分 29 秒:--list 約 90 分鐘 + 實跑
1327.625 秒(約 22 分鐘)。全程沒有殺過任何行程。
那一條紅,在第 3659 行(收工時又在第 3900 行重印一次——nextest 對失敗測試的慣例):
FAIL [ 0.051s] (3375/3595) spectyn-mesh::the_app_only_calls_routes_that_exist every_daemon_path_the_app_builds_is_served
thread 'every_daemon_path_the_app_builds_is_served' (14721714) panicked at tests/the_app_only_calls_routes_that_exist.rs:170:5:
一條跟 hub_url 同行的字面路徑都沒有 —— 這條掃描已經對不上程式碼的形狀了
跟預測一模一樣——這就是上篇留下的那條 route ratchet:掃描器只認 "/…" 開頭的字面路徑,收不到format!("{}/…", config.hub_url) 這種形狀,checked=0 撞自己的下限。它今天早就記在案、故意留紅,
等操作者裁定 memory.rs 三條路由的產品語意(D-memory)。1 leaky 沒有逐一追查,估計不影響本輪判定。
3594 passed、1 failed、70 skipped——不是零紅。而這正是這一天要交的東西。Day 11 收工時寫過兩句
話:「『跑了』跟『亮了』是兩件事」跟「正確的燈在沒人看的地方,等於壞的燈」
(day11-lights-that-never-went-red.md:443、:447)。那九盞燈裡,三盞紅得對但沒人看,四盞從沒
亮過,綠燈紀錄那時候還不存在。今天這一條紅,反過來是那兩句話的另一面:它跑了、也真的亮了——
亮成紅色,而且亮的地方有人在看、寫進了這篇文章、附了裁定點等操作者接手。一條被看見、被記名、
等著被裁定的紅燈,跟一條沒人查的紅燈,不是同一件事;而能夠誠實地把這條紅跟另外 3594 條綠分
開寫清楚,前提正是上面兩節的稅跟閘——lib 真的 Fresh 過、全量真的跑到 Summary 那一行,而不是卡
在 --list 或被錯認成別的家族。這是今天做出來的東西:不是「零紅」,是「知道剩幾條紅、為什麼
紅、紅得對不對、誰能裁」。
Day 11 把 paired_gate_check.sh 改嚴(c5948d9f):放行側要同時有簽章呼叫跟正向狀態斷言。
今天要把它接進 CI(Phase G)。第一次嘗試對 origin/main 跑,發現是飽和的——dev 分支 271 個
commit 一起看,總找得到一條放行斷言,第一次跑會綠不會紅——於是改成「受測變更的 base」(PR 用
base_ref、push 用 event.before)。然後量到比較難堪的事:
真放行也會紅。 scripts/paired_gate_check.sh:84 的 OK_PAT 是單行 regex:assert_eq!\([^;]*StatusCode::(OK|CREATED|…)。rustfmt 把帶長訊息的 assert_eq! 拆成三行,assert_eq!( 跟 StatusCode::OK 不同行,它就看不見。對 c5948d9f..HEAD——含 Day 11 的閘修正414db8c4 跟今天的紅證測試 2d062f40——跑它:exit 1,「新增的測試行裡沒有『等於成功狀態』
的斷言」。而那個範圍明明加了 the_app_callers_are_admitted.rs:103-106 跟skills_signed_caller_is_admitted.rs:165-171 兩條多行 assert_eq!(\n status,\n StatusCode::OK,。core/tests 裡這種多行放行斷言 9 處,單行 15 處。
意思是:base 一換窄,這道閘對「拆了閘沒補測試」會紅,對「補了測試而且是對的形狀」也會紅。
一道分不出放行跟繞過的閘,不是閘。接上去的第一個紅,會是這次修法 commit 自己——一個誤報。
修法在腳本,不在 yaml:awk 從 assert_eq!( / assert!( 那一行起,把同一個 hunk 裡的續行
接上,接到含 ; 的那行為止;換檔或換 hunk 就中斷(續行不在這次 diff 裡的斷言不補,寧可漏報);
接完再剝一次字串——逐行剝的時候,跨行字串每一段只有一個引號,剝不掉,接成一行才是完整的"…"。assert_ne! 不接,它本來就不算放行側。
證明三段齊(g1-okpat-proof.txt),cwd、HEAD、範圍全部不變,只換腳本:
414db8c4~1(= c5948d9f)→ exit 1,「沒有『等於成功狀態』的斷言」。assert_eq!( status, StatusCode::OK, )。assert_ne!(…401)、StatusCode::OK → exit 1(接完再剝才擋得住這種騙法);補上真的多行assert_eq!(…, StatusCode::OK, …) → exit 0。同一段合成 commit 換回原始腳本 → 仍 exit 1:origin/main 仍 exit 0;40 個 0 仍「跳過」exit 0。這輪之後,一個獨立的駁斥角色(G-1S)沒有推翻這個修法,但用 34 個合成案例攻擊它,量到兩點修正:
續行尾端的行內註解(// … StatusCode::OK)跟 tail-expr 沒有 ; 吞到下一個 fn 的兩種罕見情境,
會讓修過的版本誤放行。修正版收進 paired_gate_check.FIX.sh(也就是實際落地的版本),在
join 之前先剝掉每行尾端的 // 註解,擋掉這兩個新增的誤放。沒有全擋乾淨:格式引數裡的StatusCode::OK(assert_eq!(status, UNAUTHORIZED, "want {}", StatusCode::OK))、多行版的is_success()、跨行字串誤紅這幾個既有盲區還在——腳本自己的原則寫著「寧可漏報也不要吵到被
關掉」,這次先接受,寫進了駁斥者的報告裡,沒有藏起來。
兩份工作合起來,再對上 CI yaml 的組合方式——原本兩份 diff 同一個 hunk 各插一個 paired-gate:
job,硬併是重複 key;正確組合是 trigger 的 hunk + 新 job 的 hunk + 一支新腳本scripts/ci/paired-gate-base.sh,由它算出「這次要審的那段改動」的 base:
pull_request → origin/<base_ref> (PR 自己的 commit)
push → github.event.before (剛 push 上去的那幾個 commit)
40 個 0(第一次 push)/ 抓不到(force-push 之後) → HEAD~1
event.before 是 40 個 0 時,paired_gate_check.sh 原本的 fallback 會把它當成一個真的
(不存在的)commit,git diff 000…0...HEAD fatal 之後被 || true 吞掉、diff 變空、「跳過」
exit 0——壞成綠的,不是判定;test_count_ratchet.sh 那邊更難看,set -euo pipefail 之下git grep fatal,整支腳本直接以 exit 128 死掉,一行判定都沒印。paired-gate-base.sh 用git rev-parse --verify '<base>^{commit}' 先驗成真的 commit 才交出去,解決這個問題。
一個 commit 98afb0d7(16:31):ci-fast.yml 的 push 加 dev/**(pull_request 仍然只認[main]——PR 進 dev/** 時 origin/main...HEAD 是整條分支,前提本來就不成立);新 jobpaired-gate 接進去,對 base-ref 版本跑,不是 origin/main;新增 paired-gate-base.sh。
本機驗證(不需要 cargo):bash scripts/paired_gate_check.sh 414db8c4~1 對取代前的腳本
exit 1,對新腳本同一範圍 exit 0、印出接回後的 assert_eq!( status, StatusCode::OK, );YAML
用 loader 解析過、確認沒有重複 key、7 個 job、main-only 的 job 與 pr-serves-slug 零改動;
resolver 四種情境(push 40 個 0 → HEAD~1、pull_request base=main → origin/main 等)全部照
規格印出正確的 base。test_count_ratchet.sh 隨同接進同一個 paired-gate job。
沒有做的事,說清楚:GitHub 上的紅證——推一個拋棄式 dev/redproof-… 分支、看 paired-gate
job 真的在 GitHub 上紅過一次、然後刪掉分支——沒有做。 這不是猶豫,是這件事本身的性質:推分支
到遠端、在公開的 Actions 頁面留下紀錄、花掉 CI 的計費分鐘,這條線這個 session 不代跨。裁定點
D-phase-g-redproof 記在案,等操作者一句話。commit 訊息裡也是同一句:「先問操作者再做,不在
這個 commit 裡做。」
今天的協定是每 10 分鐘一輪、每輪 8 個 agent 開到上限。代價是 session 的用量上限撞了兩次,
兩次都是整段時間沒有任何工作被推進:
| 段 | 長 | 死掉的 | 回來的 | cron |
|---|---|---|---|---|
| 07:15–09:30 | 2h15m | 4 個:文章草稿、paired gate base-ref、紀錄查核、Day 11 勘誤 | 5 個(兩份靜態編譯審閱、移植檢查、lib commit 組裝) | 排了 13 次,0 次執行 |
| 10:45–14:30 | 3h45m | 6 個:合併 CI diff、駁斥 OK_PAT、批評者、文章 v3、SVG、governance 文件掃描 | 2 個(多行 OK_PAT、文件一致性) | 排了 20 次,0 次執行 |
六個小時、10 個 agent 死在半路。第一段剛好蓋住修之後全量收工(08:36)——它收工的時候沒有人在看。
第二段更難堪:10:45 targeted 101/101 綠、lib 修好了,然後三小時四十五分沒有人在。這一天的題目
是「讓那盞燈有人看」,而它綠的那一刻正好沒人。
死掉的 4 個第一次全部重派、都回來了;第二次死掉的 6 個 14:30 重派,全部收工。兩段的 build lock
狀態剛好相反——第一段全量在 --list,機器沒閒著;第二段 targeted 早收工了,機器空著,只是沒有人。
今天勘誤了 Day 11 的九處(5592be63,docs 版已套),歸成四句——三句是同一種錯:
/sw.js 又把 HTML shell 放進 precache」——sw.js 從 167f96c7 起就沒有 /console,assert_eq!(status, 200),第一個請求就 401。這台從沒量到 p99;今天量到了,180 ms。前三句是同一個錯:我讀了測試的名字跟 panic 訊息的第一行,沒讀 panic 的那一行在哪。 名字叫
latency,就寫機器;訊息說 precached,就寫 sw.js。
只說了一半的那句:「它跟程式碼無關,跟 binary 數量成正比,而且 lib 一改就全部重付」——前半對,
後半少了真正的原因:lib 不改也全部重付,因為 build.rs 每次都重跑。
10:33 想把勘誤版重貼回 iThome 草稿頁,才發現:Day 11 已經發表(/articles/10407645 回
200,標題「Day 11:從來沒紅過的燈,不是綠燈」)。發表出去的那份沒有這九處勘誤。已發表的文章是
公開內容,不代改。修正版 body 備好(18,418 字元,勘誤 9 句 + 一處 bullet 修正),要不要把已發表
的 Day 11 更新成勘誤版、還是在 Day 12 文內勘誤就好,是操作者的裁定——D-day11-errata。這一節
就是文內勘誤的那一半;另一半等裁定。
一、名字不是量測。 16 條紅分成五種,其中兩種是按測試名字分的:名字有 latency,就是機器;
訊息說 precached,就是 sw.js。今天讀 panic 行,兩種都不是。同一個 binary、同一個 401 body、
同一行——它是雙重驗章的第 12 條,從沒量到時間。一條紅要分到哪一種,看 panic 的那一行,不看
測試叫什麼。
二、下限要放在會少的那個粒度上。 「清點器只清點它名字裡那片」,第六次。這次特別難堪,因為
同一個測試檔裡另一條檢查已經學會了,deriver 就在幾百行下面,自己去讀了 serve.rs。而它有一條
總下限,看不出少一整個檔。修法不是往清單裡加三行,是讓 deriver 讀同一份 ROUTE_SOURCES,再給
每個檔一條下限:一個檔有 require_cluster_auth( 卻反推不出任何路由,就紅。
三、稅決定順序;但先量稅是什麼。 這台 Mac 對每一支新 binary 收 12 到 80 秒不等,202 支
一輪;所以會紅的測試先進 tree、lib 的補丁併一次 commit、build lock 被佔的時間拿來讀。順序是
對的。可是我以為 lib 重編是那筆稅的開關,F0 量出來它是另一筆稅,而且每一輪都在付。一個估錯八倍
的數字(80 分鐘 → 11 小時)跟一個沒量過的前提(lib 一改才重付)是同一種東西:寫「估」的還可以
勘誤,沒寫的連勘誤都沒得寫。
四、一道閘要分得出放行跟繞過。 對真實的 diff 跑改嚴後的 paired gate——這次修法自己——它
紅了,理由是找不到放行斷言,而放行斷言就在那裡,只是 rustfmt 把它折成三行。一道對真放行跟假
放行都說「不」的閘,跟一道都說「好」的閘,給的資訊一樣是零。這句話今天要再補一個限定:第一版
修法讓它對一段寫著 StatusCode::OK 的散文正確地說了一次「不」,但另一輪攻擊量到同一支腳本對
另外兩種「寫著 OK 的散文」(續行尾註解、跨 fn 吞行)仍然會說「好」——閘修好一次不等於閘修好,
要有人專門去攻擊它,才知道它擋得住哪些形狀、擋不住哪些。
五、一份誠實的紅,跟一份沒人看的紅,是兩件事。 Day 11 寫過:正確的燈在沒人看的地方,等於
壞的燈。今天全量收工,3594 綠、1 紅——那 1 紅不是意外,是 14:32 全量起跑前就寫下「預期紅:1
(route ratchet)」的那一條,跟預測一模一樣。這不是「終於全綠了」的故事;是「終於有一份紀錄,
能誠實地說清楚剩幾條紅、為什麼紅、誰能裁」的故事。零紅不是今天的目標,能不能把紅講清楚才是。
今天收工時,四個裁定點在等操作者一句話,沒有一個是這個 session 能代裁的:
/rpc/recall 誠實改名 / 蓋後端。98afb0d7),但沒有裁定點的下一步比較直接:白天 Gatekeeper 的稅目前只量到「豁免對已起跑的行程樹沒有立刻生效」,
還沒有量到「豁免對一次全新起跑的 --list 有沒有幫助」——下一輪從乾淨狀態起跑的全量,會是第一次
真的測到它;Phase G 的 GitHub 紅證程序已經寫好、排練過本機版本,只等 D-phase-g-redproof 一句話
就能執行;而這篇文章本身,也是今天佇列裡跟裁定點無關、可以直接做完的最後一件事。