Skip to content

ci(scripts): 002 記分守門移植進 main——pre-commit 第 6 步+CI 聚合強制(#881) - #884

Merged
s123104 merged 23 commits into
mainfrom
ci/starpuff-002-log-gate
Jul 26, 2026
Merged

ci(scripts): 002 記分守門移植進 main——pre-commit 第 6 步+CI 聚合強制(#881)#884
s123104 merged 23 commits into
mainfrom
ci/starpuff-002-log-gate

Conversation

@s123104

@s123104 s123104 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Closes #881

問題

002 記分守門(scripts/verify-002-log.mjs從未進入 main

漂移的真實形狀(修正 #881 開卡時的前提)

Issue #881 原本記載「AGENTS.mdCLAUDE.md 都明文記載 pre-commit 有第 6 步」。實際查證:origin/main 的這兩份文件從未提過 verify-002-log.mjs,它們記載的是 5 步驟 pre-commit,自身完全一致。宣稱 6 步驟的文字只存在於 experiment 分支,以及主 checkout(feat/single-fold-fluid-fitM AGENTS.md)的未提交修改。

所以漂移不是「main 文件說謊」,而是:

代理照著一道不存在的守門在工作。

因為代理載入的 always-applied rules 來自本機 checkout 那份被修改過的 AGENTS.md,而不是 main。這個形狀比「文件說謊」更值得警惕——任何讀 main 的人都不會發現它:main 自身自洽、CI 沒有缺步、hook 檔案也沒有 TODO。只有比對「代理實際載入的規則」與「main 的實況」才會浮現。PM 開卡時看到的正是本機 checkout 的內容,因而誤判為 main 的狀態。

實務後果就是這個 session 的現況:PM 與各工人加總已人工驗算 002 六七次,而 #857 的 squash 聚合檔頭錯誤(寫 +1、實際 +9)在無守門下一路走到待合併。

本 PR 讓「代理載入的規則」與「main 的實況」同時為真。

移植範圍與理由

項目 決定 理由
scripts/verify-002-log.mjs 移植 bcd52c15f 版(含 --base-ref 兩種語意共用同一 validate002 核心,無雙實作
.husky/pre-commit 第 6 步 移植,對齊 main 現況 main 的 5 步版與來源 pre-image 逐字相同,patch 乾淨套用 → 6 步
CI --base-ref 強制 移植(關鍵) 見下方實證:pre-commit 攔不到 squash 聚合記帳錯誤
24 例單元測試 移植 + 自補 2 例 掛入 test:root,pre-push 覆蓋
@types/node + scripts/tsconfig.json 移植 scripts 測試用 node 內建模組,typed lint 需要型別

CI 端為何比 pre-commit 更關鍵(實證)

PR #857 真實 head493c17fe2)逐 commit 跑 pre-commit 語意:

PASS 66ac09839 / 3733f70c7 / a5d87e181 / 0424e6894 / 9573e31d6
PASS 18dff58f1 / b7bda0f76 / 081feadbc / 024e5a4bd / dd45a3436 / 493c17fe2

11 個 002-touching commit 全數綠燈,但 CI base-ref 模式對同一 head:

002 記分守門失敗:
- 檔頭計數(reward 1、penalty 0、neutral 0)與本次新增條目(reward 10、penalty 1、neutral 0)不符
- 本次分數變化應為 9(reward - penalty),檔頭為 1
- 累計總分斷鏈:前版 242 + 本次 1 = 243,檔頭為 251
exit 1

對照組 PR #880 head(92305bede):002 記分守門通過 exit 0。

對 main 現存 002 的實跑

  • 區段內實際條目 518 筆(- ID: 共 519 行,含模板行 1 行)
  • globalErrors: []、重複 ID 0、累計 +251 檔頭解析正常
  • 218 筆歷史條目無標準 ID 前綴、3 筆有額外欄位(content_type/topics 等)→ 不擋(歷史條目格式錯誤不回溯)

解析器修正(本 PR 新增,非來源版)

數字更正(round 2 審查指出,已實跑複驗):初版描述為「5 處漏空行、6 筆條目隱形」,高估。實測結果如下,77f55adb 的 commit message 仍留有舊數字,將於 rebase 時 amend 修正。

歷史檔有 2 處漏空行使多筆條目黏成一塊(8 行 2 筆、12 行 3 筆),block.find 只取首個 ID → 3 筆條目對 ID 唯一性與「歷史條目不可刪除」防護完全隱形

  • reward-rw-theme-ssot-drift-convergence
  • reward-starpuff-v17-gamescene-strangler-debt-train(2026-07-20)
  • neutral-starpuff-t2a-changeset-release-intent(2026-07-22)

另有 3 個超過 4 行的區塊,實為單一條目帶 content_typetopicskeywordsrelated_entries 額外欄位、非黏合,不造成隱形。初版把這 3 個一併算進「漏空行」才得出 5/6。

複驗方式(舊解析器 vs 新解析器實跑 main 真實檔):

=== df2033f51 ===   舊 515 區塊 | 新 518 條目 | delta 3 | 隱形 3
=== origin/main === 舊 526 區塊 | 新 529 條目 | delta 3 | 隱形 3
真正黏合區塊: 2 | 單一條目帶額外欄位區塊: 3

修法:解析器補「- 日期:」為次要邊界(空行仍為主要邊界)。納管條目 515 → 518。歷史格式錯誤仍只對新增條目生效。補 2 案回歸鎖,拔掉邊界後轉紅、還原後綠。

故障注入驗證(對 main 現存 518 條目實跑)

# 情境 結果
0a 002 不在 staged set 跳過 exit 0
0b 正確 append(+1 / +251→+252) 通過 exit 0
1 檔頭數字與新增條目數不符 exit 1 檔頭計數(reward 3…)與本次新增條目(reward 1…)不符
2 累計總分鏈斷裂 exit 1 累計總分斷鏈:前版 251 + 本次 1 = 252,檔頭為 260
3 ID 重複 exit 1 ID 重複:「reward-build-commit-sha-ssot-zeabur」
4 刪除歷史條目 exit 1 歷史條目不可刪除,缺失 ID:「penalty-starpuff-t6-w16-shard-respawn-mistake」
5 條目缺行(非四行模板) exit 1 條目行數應為 4 行(實際 3 行)
6 日期格式錯誤 exit 1 日期格式應為 YYYY-MM-DD:「2026/07/26」
7 ID 前綴不合法 exit 1 新增條目 ID 必須以 reward-/penalty-/neutral- 開頭

新增控制項 AGT-LOG-03:002 條目必須集中在單一 commit

pre-commit 以「本 commit 新增條目」對帳、CI 以「PR 聚合淨變化」對帳,兩者在 002 分散多 commit 時必然互斥。臨時 repo 三情境實證:

情境 pre-commit CI base-ref
A 逐 commit 檔頭各自正確(+1 / +1) 全綠 (聚合應 +2)
B 末個 commit 改寫為聚合檔頭(+2)
C 單一 002 commit 承載全部條目

故新增 AGT-LOG-03 並寫入兩份文件。本 PR 自身即依此規則落盤(只有 1 個 002-touching commit)。

文件同步前後對照

位置 前(origin/main)
AGENTS.md AGT-PC-01 pre-commit 5 步驟 pre-commit 6 步驟
AGENTS.md Quality Gates 「實際 5 步驟」1–5 「實際 6 步驟」1–6(第 6 步 002 守門)
AGENTS.md 002 SSOT 無守門描述 檔頭固定格式、pre-commit 第 6 步、CI --base-refAGT-LOG-03、rebase 例外
AGENTS.md Quality Gates 無 CI 002 章節 新增「CI Quality Checks(002 記分守門,PR 專屬)」(來源分支從未寫過)
AGENTS.md 控制矩陣 AGT-LOG-03 新增
CLAUDE.md 002 SSOT 無守門描述 同步三條(pre-commit / CI / AGT-LOG-03)+ rebase 例外

殘留檢查:grep "5 步驟" AGENTS.md CLAUDE.md README.md docs/ → 0 命中。

驗證

  • vitest run scripts/__tests__/verify-002-log.test.ts26 passed(24 移植 + 2 自補)
  • pnpm test:root55 passed(5 files)
  • pnpm lint0 error(9 warning,與 main 基線相同)
  • pnpm typecheck → 全 9 專案通過
  • pnpm format → All matched files use Prettier code style
  • pre-push(typecheck + test + build:ratewise)→ All pre-push checks passed!
  • pre-commit 實跑:Step 6/6 對本 PR 自身 3 筆新增條目綠燈
  • CI 語意自檢:node scripts/verify-002-log.mjs --base-ref origin/main → 通過

附帶影響(如實揭露)

  • lockfile:root 宣告 @types/node ^24.13.3 後,原本浮動到 26.1.1 的 peer 收斂為 24.13.3,對齊 engines ^24.0.0 與 8 個 app 的宣告。純型別套件,無執行期影響。
  • scripts/__tests__/lighthouse-production.test.tsscripts/tsconfig.json 加入 node 型別後 readFileSync 回傳型別由 errorstringprefer-regexp-exec 開始生效 → String#matchRegExp#exec(行為等價,來源 commit 同樣修法)。
  • 未附 changeset(PM 已裁決確認):本 PR 零 app 原始碼變更(repo 治理工具 + 根文件),無可 bump 的 package。changesets 操作的對象是 package,硬補只會產生對使用者無意義的 CHANGELOG 條目。對照組 PR docs(starpuff): T7-C 設計文件 SSOT 收斂——主題拆分、取代對照與 #871 漂移修正 #880 保留 patch changeset,因其變更落在 apps/starpuff/docs/@app/starpuff 確有變更。此界線已寫入 AGT-VER-01 與 Phase 7(3289dc911),避免下一個人重新糾結。

002

+20(reward 26、penalty 6、neutral 2)|累計 +299 → +319(初版落盤為 +3/+254,歷輪審查收斂後的最終帳目見下方「002(最終帳目)」)

Made with Cursor


審查收斂(round 1:Codex 兩條,皆已處理並 resolved)

P2 scripts/verify-002-log.mjs — staged 刪除視為驗證失敗 → 已修 97ae594ae

真實破口,先實證後修。git rm --cached 後 index 已無該路徑,git show :<path>null,原邏輯以「不在 staged set」靜默跳過 exit 0——整份刪除 002 的 commit 可通過本地守門(hook 外層條件其實有命中,因為刪除也出現在 --name-only)。

修法:區分兩種 null——index 與 HEAD 皆無 = 與 002 無關的 commit(跳過)、HEAD 有但 index 已刪 = staged 刪除(必紅),與 runAgainstBaseRef 既有刪除判定語意對齊。

修前: git rm --cached … → 002 記分守門跳過(…不在 staged set)        exit 0
修後: git rm --cached … → …不可刪除(HEAD 存在此檔,index 已移除)    exit 1
修後: 002 未動的一般 commit → 002 記分守門通過                         exit 0

補 3 案 git 整合測試(29 passed);拔掉刪除防護後該案轉紅、還原後綠。

順帶修正一個對腳本行為的理解偏差:git show :<path> 讀的是 index,而 index 含全部 tracked 檔——所以「不在 staged set」這條分支實際只在刪除或未追蹤時觸發,訊息文字已改為「不存在於 index 與 HEAD」。

P1 AGT-VER-01 — 為品質閘門變更新增 changeset → 不補,但規則已精確化 3289dc911

見上方「附帶影響」。審查引用的 AGENTS.md:L124 正是規則不夠精確之處,已改為以「變更檔案所屬 package」劃界。

附帶一問回覆:是否評估過「讓 pre-commit 也改用聚合語意」

誠實回答:當時沒有評估。 我是在準備提交自己的 002 時撞到 pre-commit 與 CI 的語意衝突,用臨時 repo 跑出三情境後直接收斂到唯一雙綠的 C(單一 002 commit),沒有回頭問「能不能改 pre-commit 讓 A 也成立」。以下是事後補做的評估。

技術上可行。 pre-commit 若改用 merge-base(<base>, HEAD) 當基準版,情境 A 就會成立:commit 1 寫 +1/+252、commit 2 寫 +2/+253,兩者都對聚合基準對帳,末態也對——「002 逐 commit 更新」的既有 SOP 得以保留。merge-base 在分支推進時穩定(等於分支點),origin/main 略微過時不影響。

排除的理由是「base 不可靠推導」,不是「做不到」

  1. base 不恆為 mainTools/AGENTS.md 記載 ratewise 2026H2 的 PR base 一律指向 experiment/ratewise-product-2026h2。硬寫 origin/main 會對這類分支算出錯誤 merge-base → 錯誤 previousTotal → 假紅或假綠。而假綠比沒有守門更危險。
  2. 本機無權威 base 來源。PR 可能還不存在;@{upstream} 指向自己的遠端追蹤分支不是 base;要正確就得引入設定檔或環境變數,等於為了保留舊 SOP 而增加一層可被設錯的狀態。
  3. CI 已經有權威 basegithub.event.pull_request.base.sha),聚合語意在那裡是零猜測的。把同一件事在猜得到 base 的地方做一次、猜不到 base 的地方也做一次,是把不確定性引進本來確定的環節。

取捨結論:現行設計讓 pre-commit 維持「零外部依賴、只看 index vs HEAD」的確定性,把聚合判斷交給唯一知道 base 的 CI;代價是 SOP 從「逐 commit 更新」改為「累積後單一 commit」。我認為這個代價比引入 base 推導啟發式低,但這是可辯論的取捨,不是唯一解——若審查席或 PM 認為保留「逐 commit 更新」的價值更高,--base-ref 已經是現成入口,pre-commit 只需再加一層 base 解析(建議走顯式設定而非猜測),改動範圍可控。

順帶記錄一個相關的操作面後果(本 PR 自己就撞到):002 落盤後才收到的審查修正,其 commit 不得再新增 002 條目(pre-commit 必紅)。本次兩個修正 commit 因此都不動 002,審查相關的條目將在 PM 指定的 rebase 時 fold 回同一個 002 commit。此規則已補進 AGT-LOG-03

審查收斂(round 2:獨立審查席 REQUEST_CHANGES,功能 71/工程 77)

獨立審查席(與 Codex 互不知情)撞出同一個 Blocking——staged 刪除破口——佐證其真實性;該項已於 97ae594ae 修畢。其餘意見處理如下。

Should-fix 1:AGT-LOG-03 過早收斂 → 已把排除理由寫進規則本體 257a1fe76

審查席自建並驗證了 pre-commit 改用 merge-base(main, HEAD) 的第四種設計,實測多 commit 工作流能與未修改的 CI 邏輯雙綠,因此認為文件把設計取捨寫成了技術必然。這個批評成立——它反對的是包裝方式,不是選擇本身。

PM 已裁決維持現行設計(審查席假設了 merge-base(main, HEAD),但 base 不恆為 main,這點審查席未考慮)。現已在 AGENTS.md 新增小節「AGT-LOG-03 已評估但不採用的替代方案(此為設計取捨,非技術必然)」,明載:

  • 替代方案技術可行、且能保留逐 commit 寫法
  • 不採用的三個理由:base 不恆為 main(實驗線 PR base 指向 experiment 分支,硬寫 origin/main → 錯誤 previousTotal假綠比沒守門更危險)、本機無權威 base 來源、CI 已有零猜測的 base.sha
  • 代價與回頭路:若日後判定保留逐 commit 價值更高,--base-ref 已是現成入口,但必須走顯式設定、不得以 origin/main 猜測

原句也加上「在現行兩種語意下」限定詞,CLAUDE.md 同步摘要並指回本節。

Should-fix 2:runPreCommit() 零測試覆蓋 → 已覆蓋三條路徑

git 整合區塊原本每個 runGuard() 都固定帶 --base-ref,從未測過 hook 實際跑的無參數路徑。現有:

測試 覆蓋路徑
pre-commit:staged 刪除 002(git rm --cached)必紅 刪除防護
pre-commit:002 不存在於 index 與 HEAD 時跳過 唯一合法的跳過條件
pre-commit:正確 append 綠燈 正常路徑
經 symlink 路徑呼叫仍執行 main(不得靜默 exit 0) 直跑判定

runGuard 抽出 runScript(repo, scriptPath, ...args),以便同時驅動兩種語意與任意腳本路徑。

Should-fix 3:002 條目誇大破口規模 → 已更正

見上方「解析器修正」的數字更正段。審查席正確,我實跑複驗後確認 delta 為 3 不是 6、真正黏合區塊為 2 不是 5。已修正 002 條目本體、腳本註解與 PR 描述三處。

Should-fix 4:isDirectRun 對 symlink 不健壯 → 已修 257a1fe76

舊版(未 realpath)以 symlink 絕對路徑呼叫: stdout=[]                    exit 0  ← 靜默假成功
新版:                                        stdout=[002 記分守門通過]  exit 0

改為 realpathSync(process.argv[1]) 後再比對(realpathSync 失敗時退回原比較,不因此拋錯)。補 1 案回歸測試,顯式建立 symlink 而非依賴平台 /tmp 行為,故 Linux CI 上同樣有效;還原舊實作後該案轉紅。

Nit A:ID 存在性檢查不護內容完整性 → 評估後不修,理由如下

同 ID 但描述被靜默改寫確實可洗白 penalty,這個缺口屬實。但堵它會同時封死合法的精確性修正路徑——validate002 目前明確允許「無新增條目且檔頭未動」時不驗記分,正是為了 typo 與敘述更正;本 PR 的 Should-fix 3 就是走這條路徑(更正 002 條目的數字,pre-commit 綠燈通過)。

要同時擁有兩者,需要一套「修改既有條目需附理由」的機制(例如條目層 hash + 白名單,或改用 append-only 更正條目),那是獨立的設計題、且會顯著增加日常摩擦。現階段的取捨是:條目的存在與計數由守門保證,內容正確性由審查保證。若 PM 認為值得,建議另開 issue 處理而非塞進本 PR。

Nit B:錯誤訊息一律引用 block[0] 難定位 → 已修 257a1fe76

parseEntries 改先解析 ID,錯誤訊息以 ID 為定位字串(取不到才退回首行)。500+ 條目的檔案裡只引用日期行無法指出是哪一筆。行數/行前綴/日期格式三類訊息皆已帶 ID。

審查收斂(round 3:Codex 追加 P2 — 空白 ID 繞過記分)→ 已修 6c3426780

真實破口。新增四行條目但 ID 寫成 - ID:(空值)時 entry.id 留成空字串,newEntries 的 truthy 篩選把它排除、唯一性檢查也 continue 跳過,而模板/日期/前綴檢查都不觸發——只要檔頭不動就全綠,等於憑空插入一筆不計分的條目

修前: validate002(新增空值 ID 條目) -> errors: []
修後: -> ["條目 ID 不可為空:「- 日期:2026-07-26」"](空字串與僅空白皆擋)

修法:parseEntries 以 trim 後的值判定 ID,空白視同缺少 ID,措辭與「缺 ID 行」分開以利定位。補 2 案,紅綠驗證通過(32 passed)。

回溯影響為零——df2033f51origin/mainHEAD 三個參照點皆 0 筆無 ID 條目,不會回溯擋既有 commit。

與 Nit A(同 ID 內容被改寫)的區別:那條有合法用途(精確性修正,本 PR 正在用),這條沒有——空白 ID 純粹是漏洞。

審查收斂(round 4:Codex 追加 P2 — 空白原因/解法欄位)→ 已修 f88ade565

與 round 3 同族:行檢查只比對 startsWith 前綴、不看內容,故 - 原因:- 解法: 留空(含僅空白)仍全綠,讓缺 root cause 或 resolution 的紀錄通過兩道守門。

修前: validate002(新增「原因」空值+「解法」僅空白條目) -> errors: []
修後: -> ["條目「reward-blank」的「- 原因:」不可為空", "…「- 解法:」不可為空"]

修法:兩欄取值後 trim 判空(抽為 CONTENT_PREFIXES)。補 2 案,紅綠驗證通過(34 passed)。回溯影響為零(三個參照點皆 0 筆)。

四行模板的內容層檢查現已完整,無第三個同族缺口:- 日期:DATE_RE 覆蓋(空值不符格式必紅)、- ID:6c3426780 修畢、- 原因:- 解法: 本次修畢。

監控席回報:既有條目欄位可被掏空 → 已修 64bc78932(PM 裁決範圍)

已堵住 git rm 整份刪除後,「保留檔案與 ID、把條目內容清空」是等效的規避路徑,實質同樣湮滅 penalty 證據。

採用的規則(PM 裁決,範圍精準收窄):某欄位在基準版為非空時,修改後不得為空(含刪掉整行、留空值、只剩空白)。日期/原因/解法適用;ID 被掏空由既有的「歷史條目不可刪除」攔下。歷史上本來就為空的欄位維持豁免。

原因掏空          -> 條目「penalty-old-incident」的「- 原因:」原有內容不可清空
解法掏空(僅空白) -> 條目「penalty-old-incident」的「- 解法:」原有內容不可清空
日期掏空          -> 條目「penalty-old-incident」的「- 日期:」原有內容不可清空
ID 掏空           -> 歷史條目不可刪除,缺失 ID:「penalty-old-incident」(既有防護已覆蓋)
精確性修正         -> [](不受影響)

不採用「回溯驗證所有既有條目的四行完整性」——main 上有 218 筆歷史條目無標準 ID 前綴、3 筆帶額外欄位,全面回溯會讓守門對現存資料直接報錯,而修正歷史條目又觸犯不可刪改原則,形成死結。

判準:「有沒有從有變成無」,而不是「內容有沒有變」

這條分界是刻意設計,已寫進 AGENTS.md(新增小節「為什麼堵『掏空』但不堵『改寫』(此分界為刻意設計,不是漏做)」)與 CLAUDE.md 摘要:

  • 掏空 = 就地刪除 → 擋。可機械判定(非空性),誤判風險低。
  • 改寫不擋 → 與合法且必要的精確性修正無法機械區分(更正錯字、修正誤植數字是同一個操作)。守門若連內容改動一併擋下,會封死唯一的更正管道,而歷史條目又不可刪除重寫。語意品質交由審查把關。
  • 這也與先前「同 ID 內容被靜默改寫」評估後不修的結論一致,兩者是同一條分界的兩側。

回歸測試(含真實 diff 正向對照)

補 5 案(日期/原因/解法留空、既有條目整行刪成只剩日期與 ID、正向對照),紅綠驗證:拔掉掏空防護後 3 案轉紅、還原後 39 passed。Codex 描述的具體情境(既有 penalty-* 條目縮成只剩日期與 ID,整行刪除而非留空值)另有專屬回歸鎖 b70a822e1正向對照採本 PR 真實發生的精確性修正——即破口規模數字由「5 處/6 筆」更正為「2 處/3 筆」那次 diff 的實際文字,斷言 errors 為空。

另以三組真實 diff 實跑確認零誤判:

PASS 我的數字更正 diff(aa79a77b9 → 257a1fe76)
PASS 本分支全體 vs 舊 base(df2033f51 → HEAD)
PASS main 自身推進(df2033f51 → origin/main)

實作上 parseEntries 抽出 fields map(各欄 trim 後取值),供內容非空檢查與跨版本比對共用,未新增第二套解析。

監控席回報:hook 觸發面與 main push 兜底 → 已修 2d5a67322

必修 1:hook 觸發條件——一條成立、一條經實測不成立

路徑 監控席宣稱 實測 結論
git rm / git rm --cached hook 跳過 --name-only 命中 1 → hook 觸發 → 腳本擋下 宣稱不成立,此路徑原本就有防護
git mv hook 跳過 --name-only 命中 0 → hook 跳過 宣稱成立,真洞

git diff --cached 比對的是 HEAD vs index,刪除會以 D 呈現故路徑仍列出;git mv 則因 rename 偵測只列新路徑(--name-status 才看得到 R100 舊路徑 新路徑)。端到端實測:

git rm --cached: hook 條件命中 1 → 002 記分守門失敗: …不可刪除(HEAD 存在此檔,index 已移除)
git mv:          hook 條件命中 0 → (腳本未被叫起;直接呼叫則同樣必紅)

所以 git mv 的缺陷只在 hook 觸發層,腳本層判定本來就正確。

修法選了「無條件執行」而非 --name-status:這條線上反覆出現的模式是「檢查了內容,但沒檢查達成同樣效果的其他形式」,而依賴 diff 呈現正屬於這一類(--name-status 仍受 rename 偵測門檻、diff.renames 設定影響)。改為無條件呼叫、由腳本以 index/HEAD 存在性決定跳過,觸發判斷收斂成單一 SSOT。實測 002 未變更時 50-60ms,相對同一 hook 內的 pnpm typecheck 可忽略。

必修 2:main push 兜底

新增 push 事件步驟,同樣置於 install 前:

- name: Verify 002 log (main push)
  if: github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000'
  run: node scripts/verify-002-log.mjs --base-ref "${{ github.event.before }}"

基準取 github.event.before 而非 HEAD~1:前者是本次 push 前的 main tip,可涵蓋一次推多個 commit(正是直推情境的樣貌),而 HEAD~1 只看得到最後一個。merge commit 的 before 即第一父,語意等同「main 上這次多了什麼」。release commit 不動 002 故自動跳過。分支初建/force push 後 before 為全零無法解析,該情境跳過而非誤紅。

第五條繞過路徑(自主排查找到並修掉):decoy ## 條目 區段

parseEntriesfindIndex 只取第一個 ## 條目。攻擊者在真區段前插入 decoy 區段、抄齊全部歷史 ID 的四行 stub,即可滿足刪除防護;真區段從此永久落在守門視野外,後續 commit 可任意處置。已改為區段必須唯一(「## 條目」區段必須唯一(找到 N 個))。

繞過路徑排查清單(10 條內容層 + 7 條 git 層,全部實跑)

內容層(對照基準版含 penalty-old-incident):

# 路徑 結果 擋下原因
1 symlink 化 002 擋下 blob 變成目標路徑字串 → 找不到區段 + 刪除防護
2 homoglyph 前綴(- ID: 半形冒號) 擋下 ID 不被辨識 → 刪除防護
3 插入 ## 標題截斷區段 擋下 後續條目消失 → 刪除防護
4 decoy ## 條目 區段 原本未擋 → 已修 區段唯一性
5 條目搬到區段之前 擋下 刪除防護
6 改掉 ## 條目 標題 擋下 找不到區段
7 條目行首縮排 擋下 前綴不匹配 → 刪除防護
8 penalty 條目 ID 改成 reward 前綴 擋下 刪除防護 + 總分斷鏈
9 002 內容整份清空 擋下 找不到區段 + 刪除防護
10 只留檔頭刪整個區段 擋下 同上

git 層:

路徑 結果
git rm / git rm --cached 擋下(hook 觸發 + 腳本判定)
git mv 原本 hook 跳過 → 已修
symlink(index mode 120000) 擋下
git update-index --skip-worktree 非繞過路徑——index 內容不變,清空的工作區檔案根本不會被提交;守門讀 index 即讀「將被提交的內容」
.gitattributes-diff 不影響(--name-only 仍列出);無條件執行後更完全無關
改名 002 並同步改 LOG_PATH 擋下(新路徑在 HEAD 無對應 → 全部條目視為新增 → 檔頭計數不符)
git merge 產生的 merge commit hook 層不覆蓋(見下),CI 兜底

已知且刻意不補的缺口:merge commit 不走 pre-commit

實測確認 git merge 觸發的是 pre-merge-commit 而非 pre-commit,本 repo 未設前者:

3 個一般 commit 後:  3 pre-commit
merge --no-ff 後:    3 pre-commit / 1 pre-merge-commit

刻意不補 pre-merge-commitgit merge origin/main 併入 main 側的 002 條目時,staged vs HEAD 會把它們全數視為新增而誤紅——那是合法工作流。此情境由 PR CI 與新增的 main push CI 覆蓋,處置與既有的 rebase 情境一致(AGENTS.md 已載明 rebase 需手動跑守門)。

回歸測試(這輪鎖的是觸發面,不是腳本層)

測試 鎖住
pre-commit 第 6 步無條件執行守門,不得以 git diff 判斷觸發 hook 條件(只看可執行行,濾掉註解)
ci.yml 對 pull_request 與 main push 都掛守門,且置於 install 之前 CI 條件(含全零 SHA 跳過)
pre-commit:git mv 改名 002 必紅 腳本層 rename 判定
多個「## 條目」區段時回報全域錯誤(decoy 區段盲區) 區段唯一性

三項各自紅綠驗證:還原 hook 的 diff 條件 → 該案轉紅;移除 ci.yml push 步驟 → 該案轉紅;移除區段唯一性 → 排查腳本回報「未擋 1 條」。43 passed / test:root 72 passed。真實 002 三個參照點(df2033f51origin/mainHEAD)globalErrors 皆為空、真實 diff 零誤判。

審查收斂(round 5:Codex 三條,含一條 P1)→ 已修 d93cbef7e

P1 main push 兜底對它自己要防的情境無效

runAgainstBaseRef 一律取 merge-base(ref, HEAD)。force push 時 before 不是 HEAD 的祖先,merge-base 會退回更早的共同祖先,使 before 與祖先之間新增的 penalty 條目在改寫後消失也驗不出來。臨時 repo 重現:

祖先 A 含 reward-a;B(before) 另含 penalty-evidence;force push 回 A 另起一條不含該 penalty 的線
新 HEAD 不含 penalty-evidence: 0
--base-ref $BEFORE  ->  002 記分守門通過   exit 0     ← 湮滅成功

修法:新增 --base-commit 直取該 commit 作基準,ci.yml push 步驟改用之;PR 仍用 --base-ref 走 merge-base(base 分支會前進,需退回分岔點)。兩者不可混用已寫入 AGENTS.md。回歸鎖:同情境下 --base-ref 綠而 --base-commit 必紅(訊息含 penalty-evidence)、基準無法解析必紅、ci.yml 結構鎖禁止 push 步驟使用 --base-ref

P2 檔頭正負號未強制

[+-]? 讓「本次分數變化:1」「累計總分:171」通過,與文件所定的固定 +N+T 不符。改為強制顯式符號;負值仍可解析(penalty 多於 reward 時 delta 為負是合法的)。讀前版總分的寬鬆 HEADER_TOTAL_RE 維持不動——它負責歷史自由格式相容,收緊會讓舊檔頭讀不出前版總分而誤紅。

P2 CLAUDE.md 與 hook 相斥

把 hook 改無條件執行時只同步了 AGENTS.md本 PR 主題正是消除文件與實作漂移,卻在同一份 PR 內自製一處——維護者依該句把 hook 改回條件式即重新引入 git mv 缺口。已同步並全文掃描確認無殘留(grep 命中 0),另記為 002 penalty 條目。

locale 依賴收斂 → 已修 525ddcc69

重現

判別讀 git stderr 文字,而 git 內建 gettext 翻譯會跟隨呼叫端 locale(execFileSync 未傳 env 即繼承)。本機 macOS Homebrew git 2.55 完整重現:

LC_ALL=zh_CN.UTF-8 git cat-file -e HEAD:nonexistent.md
致命错误:路径 'nonexistent.md' 不在 'HEAD' 中        ← 不 match 任何 pattern

端到端「初始 commit」(AGENTS.md 明載支援):
LC_ALL=C.UTF-8      → 002 記分守門通過                          exit=0
LC_ALL=zh_CN.UTF-8  → 執行期例外——…致命错误:无效的对象名 'HEAD'。  exit=1

一處與審查席環境不同:LANGUAGE=zh_CN 單獨在我機器上沒有觸發(仍輸出英文),只有 LC_ALL 會。修法同時涵蓋兩者,故不影響結論,但如實記錄差異。

修法

四處 git 呼叫(cat-fileshowmerge-baserev-parse)統一傳 env: { ...process.env, LC_ALL: 'C', LANGUAGE: 'C' }

註解記錄了審查席標為低信心殘留的那項:鎖 C locale 同時鎖住訊息穩定性,等於把「跨 git 版本翻譯字串變動」這個無法實測的風險一併降低。

回歸測試(三案)

測試 性質
所有 git 呼叫都必須鎖 C locale 結構鎖——CI 上真正有效的那道,新增第五處呼叫忘記帶 env 即紅
非英文 locale 下「尚無 commit」仍正確跳過 行為對照
非英文 locale 下 staged 刪除仍必紅 行為對照

非英文環境怎麼驗證的:本機 locale -azh_CN.UTF-8,且 Homebrew git 2.55 內建 gettext 翻譯(share/locale 下有語系目錄),所以行為對照在本機是真測試。但 CI(ubuntu runner)不保證裝有中文 locale——沒有翻譯時 git 本來就輸出英文,那兩案會變成不會誤紅也不提供額外保證的 no-op。這正是我另外加結構鎖的原因:它不依賴 locale 可用性,在任何環境都能擋住回歸。紅綠驗證:拿掉任一處 env: GIT_ENV → 結構鎖轉紅。

七情境 × 雙 locale 實跑,行為完全一致:

                                    C.UTF-8   zh_CN.UTF-8
尚無 commit                          exit 0     exit 0
002 在 index 與 HEAD 皆不存在         exit 0     exit 0
staged 刪除                          exit 1     exit 1(不可刪除)
git mv 舊路徑                        exit 1     exit 1(不可刪除)
壞 git stub                          exit 1     exit 1
GIT_DIR 無效                         exit 1     exit 1
.git chmod 000                       exit 1     exit 1

002 重算

條目 main 574 -> 本分支 598 | 新增 24(reward 16/penalty 6/neutral 2)
a+b+c = 24 == 24                OK
N = a-b = 10 == delta 10        OK
T = 286 + 10 = 296 == total 296 OK
重複 ID []   缺失 main 條目 0   globalErrors []

第十三破口:orphan HEAD → 已修 00f47209c

方法層置換只覆蓋了 path 存在性,「無基準版」的語意仍綁在「HEAD 不是 commit ⇒ null」。重現(標準 Git 指令、無 PATH 劫持):

對照組(正常 main HEAD 掏空)  → exit 1  歷史條目不可刪除:reward-a、penalty-must-survive
git checkout --orphan evil
  rev-parse --verify HEAD → 失敗
  rev-list -n 1 --all     → e8d62918(歷史 commit 都還在)
  掏空 002                → 修前 exit 0 ★,penalty-must-survive 消失
                          → 修後 exit 1「ref『HEAD』無法解析,但 repo 已有 commit——不可視為無基準版」
真正尚無 commit 的 repo    → exit 0(仍合法放行)

修法未加任何訊息 pattern:改由 git rev-list -n 1 --all 是否為空來證明「無基準版」。ref 不解析但 repo 已有 commit(orphan/損毀 symref/基準 ref 失效)一律 fail-closed。

:373 那句「repo 已確認可用,故 rev-parse 失敗只可能是該 ref 不存在」確實是假的,已刪除並改寫為正確判準。

過時註解清除與措辭鎖檢討

:342-349 那段(宣稱「必須靠 stderr 區分」「窮舉…四種合法不存在訊息」)已整段刪除。

關於它躲過我自己建的鎖——這個諷刺值得認真對待。原因是字面比對:我鎖「窮舉實測過的四種」,留下的是「窮舉守門實際會遇到的四種」,關鍵詞之間插入修飾語就漏接。

改為 regex 以主張骨架比對(/窮舉[^,。\n]{0,12}(四種|訊息)//(必須|判別[^,。\n]{0,8})靠\s*stderr//rev-parse 失敗只可能是/),實測能抓到改寫版。

但我也在測試註解與 AGENTS.md 寫明它的能力邊界,避免下一任高估:這個鎖只擋得住已知的舊主張,擋不住全新的錯誤敘述;而放寬到能涵蓋所有改寫就會誤傷「刻意描述被否決做法」的正確文字——本守門的註解大量這樣寫,我在收斂過程中就三度誤觸自己的鎖。真正的防線是鎖住行為的結構測試,措辭鎖是補漏用的衛生檢查。

結構鎖升級為 AST

採用 AST(typescript compiler,與 #886 同一套思路):

  1. child_process 只能具名且未改名匯入;禁 namespace/default,因為 cp['execFileSync'](…) 無法靠名稱追蹤
  2. 走訪全檔 CallExpression,所有對該綁定的呼叫必須落在 git() wrapper 的節點範圍內
  3. wrapper 必須帶 env: GIT_ENV

紅綠驗證兩種舊鎖漏接的繞法:

import { execFileSync as run } + run(   → 必紅
wrapper 外新增 execFileSync('git', …)   → 必紅

assertRepoUsable 敘述修正

原敘述隱含「確認過就安全」;改為明說:只在首次使用時確認一次,之後 ls-* 若因 repo 中途不可用而非零離開仍會 fail-closed,故不需重複確認。被 orphan 證偽的「rev-parse 失敗只可能是 ref 不存在」推論已從程式碼與 AGENTS.md 一併移除。

驗證

vitest 78 passed(orphan 掏空必紅、真空 repo 仍放行、AST 鎖兩種繞法、措辭鎖改寫版各自紅綠)/test:root 107 passed/全工作區 pnpm test 通過/lint 0 error/typecheck 全過/三種 git 環境失敗維持 fail-closed/10 條繞過路徑排查全數擋下。

第十二破口 → 改換方法而非再加 pattern(d4968b690

為什麼不加第五條 pattern

cat-file -e 把「物件不存在」表達成 status 128 + 人類可讀 fatal,與「repo 不可用」共用同一離開碼,只能比對英文訊息。這條路徑連續破三次(漏訊息種類 → 依賴英文輸出 → 又漏第五種),根因是把人類可讀輸出當成 API 契約。加 pattern 只是在脆弱基礎上打地鼠。

兩種探測法逐情境實測對照

情境 cat-file -e ls-filesls-tree
index 有檔 exit 0 exit 0, out=path
index 皆無 exit 128 +訊息 exit 0, out=""
git rm --cached exit 128 +訊息 exit 0, out=""
HEAD 有檔 exit 0 exit 0, out=path
HEAD 無檔、磁碟無 exit 128 +訊息 exit 0, out=""
HEAD 無檔、磁碟有(第十二破口) exit 128 +訊息 exit 0, out=""
sha tree 無此路徑 exit 128 +訊息 exit 0, out=""
尚無 commit exit 128 +訊息 exit 128
環境失敗 exit 128 +訊息 exit 128

六個「不存在」情境有五個變成 exit 0 空輸出——從例外變成正常回傳值,脆弱性從根上消失。只剩「尚無 commit」需另判,而那可用 rev-parse --verify 的離開碼結構判定,同樣不碰訊息。

實作

rev-parse --git-dir 確認 repo 可用(僅一次),之後 index 用 ls-files、tree 用 ls-tree --name-only;非零離開一律環境問題並 fail-closed。全檔零訊息比對MISSING_OBJECT_PATTERNS 整組刪除——句點錨定(invalid object name 'HEAD'\.)的問題隨之消失,不需單獨修。

LC_ALL=C 保留但已非正確性依據,僅使 git 輸出恆為決定性;此點在文件註明,避免下一任誤以為訊息比對因此安全。

git() wrapper 收斂(應修)

同意「與 acquirePooled 是同一個模式」。GIT_ENV 收進全檔唯一的 git() wrapper,結構鎖由「逐一檢查每個呼叫點是否帶 env」(字串切片,可用雙引號/spawnSync/間接呼叫/把 env 推到 220 字元後繞過)改為斷言全檔只有一個子行程 API 呼叫且必須在 wrapper 內——讓忘記帶 env 在結構上不可能,而非事後偵測。另補一道結構鎖禁止退回訊息比對。

回歸測試(兩個新情境)

pre-commit:repo 已有 commit、首次加入 002 應放行     → exit 0
--base-ref:PR 首次加入 002(基準 tree 無檔)應放行   → exit 0

紅綠驗證:還原成 cat-file 訊息比對實作後 4 案轉紅(兩個新情境 + 兩道結構鎖),還原後 76 passed。

驗證

vitest 76 passed/test:root 105 passed/全工作區 pnpm test 通過(ratewise 2773)/lint 0 error/typecheck 全過/三種 git 環境失敗維持 fail-closed/10 條繞過路徑排查全數擋下。

(附記:一次 pre-push 因 apps/nihonname 滿載 timeout 失敗,本分支對 apps/ diff 為空,重跑全綠,未以 --no-verify 繞過。)

修復 #857 帶進的 002 損壞(PM 裁決,分開 commit)

守門在上線前最後一刻被它要守的資料示範了一次存在理由。獨立驗算確認 PM 的數字:#857 相對 b7cd80b6b 實際聚合 30 筆/reward 25、penalty 4、neutral 1、淨 +21a+b+c=30 等於新增條目數、265+21=286),累計 286 正確、僅 delta 行寫成 +1。

第一步 8ee2ebc8a(純修復,不新增條目)

-> 本次分數變化:+1(reward 1、penalty 0、neutral 0)|累計總分:+286
+> 本次分數變化:+0(reward 0、penalty 0、neutral 0)|累計總分:+286

+- 日期:2026-07-26
 - ID:penalty-starpuff-invariant-hero-form-blind-spot

補回日期後該條目 parseEntries 格式錯誤由 4 條歸零,條目總數維持 574(原本即以 ID 行計為一筆)。

delta 行寫 +0 而非裁決的 +21——這裡我偏離了指示,理由如下:檔頭語意是「本 commit 新增條目的淨變化」,不是歷史聚合。寫 +21 實測必紅:

檔頭計數(reward 25、penalty 4、neutral 1)與本次新增條目(reward 0、penalty 0、neutral 0)不符
本次分數變化應為 0(reward - penalty),檔頭為 21
累計總分斷鏈:前版 286 + 本次 21 = 307,檔頭為 286

唯一能寫入 +21 的方式是 --no-verify,而那正是這道守門存在的理由,我不做。且該欄位下一個 commit 即被我的聚合覆寫,是滾動欄位而非永久紀錄。+0 對這個零新增的修復 commit 精確,同時移除了原本不實的 +1#857 的正確聚合 +21 改記在事故條目裡,那才是 durable 的紀錄。若你仍要 +21 進檔頭,需要放寬守門或授權繞過,請裁決。

第二步:事故條目

新增 penalty-002-log-audit-record-corrupted-by-squash-merge,寫明兩處損壞、實際聚合數字、守門在上線前偵測到、修復方式,以及為何 +21 不寫入檔頭。

記為 penalty 而非 neutral:002 記分是 repo 層級的品質總帳,稽核紀錄自身遭損壞應該有成本,否則誘因會偏向少報——這與「該缺陷非本 PR 實作所致」並不衝突。

第三步:rebase 至 60d151992(累計 +286)並重算

條目 main 574 -> 本分支 597 | 新增 23(reward 15/penalty 6/neutral 2)
a+b+c = 23 == 23                    OK
N = a-b = 9 == delta 9              OK
T = 286 + 9 = 295 == total 295      OK
重複 ID []  缺失 main 條目 0  globalErrors []
CI 語意(vs main): []      pre-commit 語意(vs 修復 commit): []

修好之後不再需要依賴「歷史條目不回溯」這個特性——該條目現在格式完整。

分支上有 2 個 002-touching commit(修復 1 + 條目 1),符合 AGT-LOG-03:該規則約束的是「新增條目」集中在單一 commit,修復 commit 新增 0 筆。

第十破口收斂 → 已修 b88eaeb68

Blocking:gitShow 把環境失敗當「物件不存在」

git 用同一個 status 128 同時表示「路徑不存在」與「repo 不可用」,而舊碼只排除 ENOENT。三種環境失敗實測全部 fail-open:

修前                                    修後
壞 git stub (exit 1)   exit=0  ★       exit=1  fail-closed
GIT_DIR 無效           exit=0  ★       exit=1  fail-closed(帶出 not a git repository)
.git chmod 000         exit=0  ★       exit=1  fail-closed
002 真的不存在         exit=0          exit=0(合法跳過,未回歸)
PATH 無 git            exit=1          exit=1(上輪已修,未回歸)

修法與複審建議略有不同:建議的 regex 只含 does not exist,會誤擋「尚無任何 commit」的初始 commit 路徑(該情境 git 回 invalid object name 'HEAD'.)。我先窮舉實測守門實際會遇到的全部四種合法「不存在」訊息,改用逐條列出的顯式清單:

情境 stderr
指定 tree(HEAD/sha)無此路徑 path '…' does not exist in '…'
index 與磁碟皆無 path '…' does not exist (neither on disk nor in the index)
staged 刪除(git rm --cached path '…' exists on disk, but not in the index
尚無任何 commit invalid object name 'HEAD'.

第三種是我最初的 regex 也漏掉的——git rm --cached 的測試立刻轉紅才抓到,正好證明窮舉比推測可靠。

catch 區塊逐一稽核(四處)

位置 吞掉的錯誤 可安全忽略的理由 處置
gitShow 僅上表四種訊息 各自對應一個窮舉實測過的合法「物件不存在」情境 其餘一律 rethrow
merge-base 無(立即 exit 1) 改為帶出 git stderr:原本「ref 打錯」與「repo 壞掉」印同一句,會誤導診斷
rev-parse 無(立即 exit 1) 同上
isDirectRun realpath 例外 說不出來 改為不吞,由統一入口 fail-closed

isDirectRun 原本吞掉例外並退回字面比較,比不中就靜默視為「非直跑」——main() 不執行卻 exit 0,正是假成功。該情境執行期無法穩定構造(node 必須先讀到檔案才能執行),故以結構鎖把關而非行為測試,這點如實標註。

必修 2:文件矛盾——擴充關鍵字後多抓到一處

AGENTS.md:217CLAUDE.md:109 的「不依賴解析是否成功」與實作相反。加入被取代的舊措辭後,另掃出腳本 :204 同一問題(複審未點名),共修 4 處。

必修 3:把判斷題變成清單題——並且機械強制

不只寫進文件,直接做成測試:固定六個檔案的掃描範圍 × SUPERSEDED_PHRASES 已取代措辭表,任一命中即紅。下一任不需要判斷「什麼算涉及守門行為」,唯一要做的是改寫行為時把舊說法加進清單。

故意在 AGENTS.md 塞回「約 50ms」 → × AGENTS.md 不得殘留已被取代的措辭
還原後 → 70 passed

(測試檔自身在掃描範圍內,故剔除清單宣告區段避免自我命中。)

應修 4

headHeaderLineheadContent ? 已對齊 hasBase

回歸測試

新增 5 案(壞 git stub、GIT_DIR 無效、.git 權限、isDirectRun 結構鎖、措辭漂移檢查),各自紅綠驗證。vitest 70 passed/test:root 99 passed,10 條繞過路徑排查與 fail-closed 掃描維持全綠。

複審收斂(兩席 APPROVE 91/88)→ 已修 8d49fc82c

全 repo 關鍵字掃描(上輪範圍太窄,這輪擴到整個 repo)

rg --hidden預設會跳過 .husky.github 等隱藏目錄,這正是上輪漏掉 hook 的原因)掃 verify-002-log|002 記分守門|AGT-LOG-0|base-ref|base-commit

檔案 是否描述守門行為 處理
scripts/verify-002-log.mjs 是(檔頭註解) :「基準版無法解析時跳過」與實作相反;唯一性範圍
.husky/pre-commit 是(第 6 步註解) :「約 50ms」
.github/workflows/ci.yml 是(兩個步驟註解) :「毫秒級跳過」
AGENTS.md 4 處(開銷、CI 章節「毫秒級」、唯一性範圍、刪除比對範圍、失敗契約表)
CLAUDE.md 1 處(唯一性與刪除比對範圍)
scripts/__tests__/verify-002-log.test.ts 是(測試註解) 對照後無失準
docs/SEO_MASTER_SSOT.md:15 否(歷史 changelog 提及 AGT-LOG-01 編號,未複述控制內容) 無需處理
package.json:36 否(僅測試路徑) 無需處理

共修 8 處。最終掃描 約 50ms|毫秒級|基準版無法解析時跳過|p50 約 120 全 repo 零命中。

必修 4:gitShow 契約閉合——你問的那題答案是「巧合」

複審席正確。我另找到一個可直接觀測的形式:把 git 移出 PATH 後整道守門靜默通過。

修前  隔離 PATH(無 git)→ exit=0  002 記分守門跳過(…不存在於 index 與 HEAD)
修後  隔離 PATH(無 git)→ exit=1  002 記分守門失敗:執行期例外——spawnSync git ENOENT

修法:先 git cat-file -e 判存在性,存在卻讀不出即向上拋,直跑入口統一捕捉為 fail-closed。

重跑複審席那個攻擊(重寫檔頭對齊 compliant 條目、寫入 +999999):

compliant 條目數: 341 (reward 268 penalty 34 neutral 39)
headContent=null 時   : []          ← 設計允許(無基準可對帳,兩席已判定)
headContent=真實檔時  : 4 errors    ← 有基準即必紅

契約閉合後 headContent=null 只可能發生在物件確實不存在git cat-file -e HEAD:002 已驗證存在),故該攻擊在真實 repo 無法觸發。

必修 5:區段外獨立 - ID: 行造成刪除假陽性

修後  移除區段外範例行 `- ID:penalty-ghost` → errors: []

改為基準版可解析時只採信解析結果;不可解析時才退回全檔原始文字掃描(CASCADE 防護不受影響,且該情境本就 fail-closed)。

必修 6:ID 唯一性休眠缺陷

歷史已重複、本次不動 002  : []        ← 不再卡死
歷史已重複、本次新增一筆  : []
本次新增造成重複          : ID 重複:「reward-a」  ← 仍必紅

應修 7:headContent 判斷統一

統一為 != null;空字串基準改走解析 fail-closed(實測回報「基準版 002 無法解析」)。

必修 3:開銷表述——採用你偏好的方案

四方觀測跨越一個數量級(p50 54ms/148ms/161ms、mean 109ms、max 曾 784ms),任何雙位數區間都會有人落在區間外。放棄具體數字,只保留「遠低於同一 hook 內的 pnpm typecheck,對體感無影響」——這個數字的用途本來就只是說明不影響體感。

建議:殘餘風險移出控制矩陣

同意「表格排版本身在對抗想傳達的訊息」。AGT-LOG-04 編號取消,改為獨立小節「已知殘餘風險(非自動化控制,不列入上表)」,開頭明言上表每一列都有自動化或流程強制、以下三項沒有:語意掏空/內容對調、無基準版首次加入 002、before 全零跳過。

回歸測試

新增 5 案(git 不可執行必紅、區段外 ID 行移除不誤報、歷史重複不回溯、新增造成重複必紅、空字串基準 fail-closed),各自紅綠驗證。vitest 60 passed/test:root 89 passed,10 條繞過路徑排查維持全數擋下。

終審收斂(Sonnet 79/84、Grok 64/73)→ 已修 6ef05cca2

Blocking — CASCADE:基準版解析失敗時刪除防護被完全掏空

parseEntries(headContent) 只取 .entries 丟棄 globalErrors,基準版不可解析時 headEntryIds 為空集合,刪除檢查落入真空。重現(檔頭算術刻意保持正確,讓刪除檢查成為唯一防線):

對照:good HEAD → wipe                擋下(歷史條目不可刪除:penalty-evidence、reward-a)
CASCADE-head-two-section-then-wipe    ★LEAK  errors=[]
CASCADE-head-no-section-then-wipe     ★LEAK  errors=[]

雙保險修法:基準版有 globalErrors 即 fail-closed;刪除比對另取「解析結果 ∪ 原始文字掃描 - ID:」不依賴解析成功。修後兩道各自獨立命中。

修法本身造成的回歸(自行抓到並修):raw 掃描讓被隱藏的條目仍出現在 staged 集合,使「插入 ## 標題截斷」「條目搬到區段之前」兩條既有防線失效——繞過路徑排查腳本當場報「未擋 2 條」。補 hiddenIds 檢查(基準版解析得到、待驗版只剩原始文字者一律擋),10 條排查回到全數擋下。

同族第八條(自主稽核發現):基準版讀不出累計總分時靜默跳過總分鏈

基準版缺檔頭 → 總分鏈是否被驗: ★未驗(靜默跳過,任意總分皆可寫入)

改為僅「無基準版」(初始 commit)才跳過,讀不出即 fail-closed。

HIGH — 雙 flag 互斥

原迴圈先匹配 --base-ref 使誤用得到假綠。改為同時指定即失敗。

錯誤路徑 fail-closed 全面掃描(13 條實跑)

情境 結果
基準版區段重複/缺失/讀不出累計總分 fail-closed
待驗版區段缺失/重複/檔頭缺失/缺正負號/整份空字串 fail-closed
--base-ref--base-commit ref 無法解析、缺值、雙 flag fail-closed
git rm --cachedgit mv fail-closed
gitShow('HEAD:…') 非預期回 null(誤判為初始 commit) 對真實 002 仍 fail-closed(229 errors)
002 未變更/index 與 HEAD 皆無 002/before 全零 放行(僅此三種)

契約已以表格寫入 AGENTS.md,供新增分支時遵循。

文件逐句對照掃描:共修 12 處

列舉兩份文件中提及此守門的每一句再逐句對實作,而非單點修補:

# 位置 修正
1 AGENTS.md AGT-LOG-01 「每次 git commit 前更新」→「每個 PR 更新,落盤時機依 AGT-LOG-03」(與 AGT-LOG-03 字面衝突)
2 AGENTS.md 控制矩陣 新增 AGT-LOG-04 殘餘風險列
3 AGENTS.md Phase 4 步驟 1 補「條目累積後於單一 commit 落盤」
4 AGENTS.md 記分段 移除「每次」語意、對齊 AGT-LOG-03
5 AGENTS.md 第 6 步 總分鏈跳過條件收斂為「無基準版」+讀不出即 fail-closed
6 AGENTS.md 區段唯一段 新增 CASCADE fail-closed 與第二道保險說明
7 AGENTS.md hook 開銷 「約 50ms」→ 實測 p50 120–160ms(n=30,附量測條件)
8 AGENTS.md CI 章節 補雙 flag 互斥
9 CLAUDE.md:107 移除「每次 commit 前」、對齊 AGT-LOG-03
10 CLAUDE.md Execution SOP 第 6 步 補「條目集中單一 commit」
11 CLAUDE.md:109 補 CASCADE fail-closed 與第二道保險
12 CLAUDE.md:113 舊語意(讀起來像 push 也用 --base-ref)→ --base-commit 直取基準+互斥

另補模組頂部註解為三模式(原只寫 --base-ref/merge-base)。

開銷數字

三次獨立量測:Sonnet mean 109ms、Grok p50 118ms、本機 n=30 p50 161ms(min 101/max 350)。文件改為「p50 約 120–160ms(n=30,含 node 啟動,隨機器而異)」。

殘餘風險已標註

新增 AGT-LOG-04:語意掏空/內容對調(ID 與非空性都保留、只改敘述)守門不擋,明列為人工審查責任範圍,指向「為什麼堵掏空但不堵改寫」小節。

Rebase 至 085152880#883 合併後)與 002 重算

衝突面如先前預測docs/dev/002... 一檔,實作檔全數乾淨套用。與 #883 的變更檔零交集comm -12 為空)——#883 只動 apps/starpuff/**,而本 PR 零 apps/ 變更;守門與其測試對 starpuffassetPlanSHARED_LEVEL_KEYSassets.ts 的引用計數為 0,僅讀 002、.husky/pre-commit.github/workflows/ci.ymlpnpm --filter @app/starpuff test 979 passed 覆蓋 #883 帶入的不變式。

002 收斂:依 AGT-LOG-03 全部條目集中於單一 commit 並置於分支最後

rebase 時把 002 從兩個中途 commit 全部退回 main 版本,改由末端單一 commit 承載。round 5 修正後亦重排使 002 commit 維持在最後。git log -- docs/dev/002... 在分支上只有 1 個 commit。

手動驗算(git rebase --continue 不觸發 pre-commit,故獨立驗算)

條目數      base(main 085152880) 544 | 本分支 555 | 新增 11
globalErrors []                      重複 ID []        無 ID 者 0
歷史條目缺失 0                       新增分類 reward 8 / penalty 3 / neutral 0
a+b+c = 11  應等於新增條目數 11  -> OK
N = a-b = 5  應等於檔頭 delta 5  -> OK
T = 前版 265 + N 5 = 270  應等於檔頭 total 270  -> OK
validate002: []

檔頭:> 本次分數變化:+5(reward 8、penalty 3、neutral 0)|累計總分:+270

條目整理:七項修正依性質分組為 4 筆 reward(非逐項流水帳)

條目 涵蓋的修正
reward-002-log-gate-content-emptiness-bypasses 空白 ID、空白原因/解法、既有欄位掏空(含整行刪除)——同屬「前綴通過但內容為空/從有到無」
reward-002-log-gate-trigger-surface-bypasses git mv hook 跳過、main push 無 CI gate、decoy 區段盲區——同屬「不改內容、改達成形式」,附 17 條排查
reward-002-log-gate-direct-run-and-locator symlink 直跑靜默假成功、錯誤訊息定位
reward-002-log-gate-changeset-scope-boundary AGT-VER-01 適用界線文件化
reward-002-log-gate-push-base-and-header-sign main push 基準語意、檔頭正負號強制

另記 2 筆 penalty(自陳,非審查要求):

  • penalty-002-log-gate-overstated-bypass-scale:破口規模數字高估近一倍(5 處/6 筆 → 實為 2 處/3 筆),由審查席實跑複驗才發現
  • penalty-002-log-gate-git-env-leak-authorship:排查用 GIT_AUTHOR_* 環境變數洩漏持久 shell,汙染一個 commit 的作者身分(未推上遠端,已 --amend --reset-author 修正)
  • penalty-002-gate-doc-impl-drift-claude-md:hook 改無條件執行時漏同步 CLAUDE.md,在一份以消除漂移為主題的 PR 內自製漂移

首個 commit message 殘留的舊數字(「5 處漏空行使 6 筆條目」)已於本次 rebase 一併更正為「2 處漏空行(8 行 2 筆、12 行 3 筆)使 3 筆條目」,以 cherry-pick 鏈非互動式重建(未使用 -i),重建後樹與重建前 git diff --stat 為空、11 個 commit 作者全數一致。

CI 首次完整跑(先前因 002 衝突無法產生 merge ref,3289dc911 之後只跑 CodeQL)

兩個新步驟行為與設計完全一致:

Verify 002 log (vs merge-base) = success   ← PR 事件執行
Verify 002 log (main push)     = skipped   ← 非 push 事件正確跳過
Install dependencies = success | Lint = success | Type check = success

GitHub 接受兩個步驟定義(含全零 SHA 判斷的 if 表達式),workflow 語法有效。11 checks 全 pass,mergeable=MERGEABLE state=CLEAN

全閘門(rebase 後重跑)

vitest 43 passed|test:root 72 passed|@app/starpuff 979 passed|pnpm lint 0 error(9 個既有 warning)|pnpm typecheck 全過|pnpm format 通過|10 條繞過路徑排查全數擋下|守門對合併態 main 的 544 條目解析零錯誤、無重複 ID、無空白 ID

合併順序與 rebase(PM 裁決)

#880 → #883 → #884 → #857#880#883 均已落地,本 PR 已 rebase 至 085152880 並依 PM 提供的實測累計值 +265 重算:最終 delta +5、累計 +270

收斂輪(Grok 收官 5 項跟進)→ 13695c9cefc0914a289666bfcae

Grok 收官確認 90/92 APPROVE 後,依「全席 ≥95」硬規則把 5 條跟進項全數收掉:

  1. 【HIGH】hasAnyCommit 本地假綠:orphan+刪光 named refs(branch -D+清 packed-refs,甚至再 expire reflog)會讓 rev-list --all 回空、但歷史 commit 物件仍在,本地守門誤判「無基準版」而讓掏空 002 exit 0(CI 走 --base-commit 同情境必紅,屬本地假綠)。判準升級 refs/reflog/object store 三層結構探測--all --reflog 為空時再以 cat-file --batch-all-objects 探測 commit 型別物件)。臨時 repo 實證:兩繞過情境修前 exit 0、修後 exit 1;真空 repo(零 commit)維持放行。commit 物件被 prune(reflog expiregc --prune=now)的極端情境本機與真空 repo 結構上無法區分——已登記 AGENTS.md 殘餘風險表,由 CI 以 GitHub 事件基準 SHA 在乾淨 clone 上兜底。
  2. 【MED】AST 鎖間接繞法:檢查抽為 auditGitWrapperStructure 單一函數。堵變數別名(綁定識別字只能作 import 綁定或直接呼叫 callee)、evalnew FunctiongetBuiltinModule/動態 import/字串夾帶 API 名;env: GIT_ENV 與 GIT_ENV 定義本身升級 AST PropertyAssignment 驗證(舊字串 includes 會被註解裡的字面假陽性通過)。12 條 mutation 元測試釘住每條繞法必紅。
  3. 【MED】文檔漂移SUPERSEDED_PHRASES 舊常數名修正(實碼已改 SUPERSEDED_PATTERNS),並把舊常數名與舊「無基準」判準敘述加進措辭鎖自我偵測。
  4. 【LOW】AGENTS 無基準語意:「只有物件確實不存在才算無基準」改寫為與三層探測實作同構。
  5. 【LOW】unrelated histories 診斷:merge-base 以 exit 1 表示無共同祖先(documented 行為),orphan PR 給專用診斷訊息(原訊息尾巴為空 stderr、無從診斷),fail-closed 行為不變。

CI 紅燈與單一聚合 commit 重建(本 PR 自己的守門擋下自己,已閉環)

收斂輪首次落盤把 002 檔頭寫成「相對分支上一個 002 commit 的增量」(+4),但 CI --base-ref 語意是「PR 最終態 vs merge-base 的聚合淨變化」(當時應為 +18)——Quality Checks 紅在本 PR 自己引入的守門上,親身示範了 AGT-LOG-03 記載的逐 commit/聚合語意互斥。

修法採根本解而非兜數字:分支上 3 個動 002 的 commit(#857 純修復+兩批條目)全部 drop,尾端重建單一聚合 commit 承載全部條目。單一 commit 結構下 pre-commit 語意(vs parent)與 CI 語意(vs merge-base)必然重合(Sonnet 席以 git reset --soft 獨立實測證實非僥倖)。重建後 Quality Checks 轉綠(8m6s 全鏈跑完)。

複審輪(Grok 93/93 的 4 項,依 PM 逐條裁決執行)

  1. node:child_process/promises 靜態 import(Grok 標 HIGH)→ PM 技術否決,降級為防禦性加固:PM 親測該子路徑不是 Node 內建模組——寫進 wrapper 檔的實際後果是 ERR_UNKNOWN_BUILTIN_MODULE 載入失敗 → pre-commit 第 6 步大聲 fail-closed,不是靜默繞過;且既有「匯入必須恰為 execFileSync」斷言已擋住 spawnSyncexec 等真實存在的同族 API(故 Grok 建議的擴充禁用清單不採納)。仍以 1 行成本把 specifier 匹配放寬為「路徑段含 child_process 者皆檢」(/(^|[:/])child_process(\/|$)/)並補 mutation,徹底消滅此爭點——程式註解與 002 條目均誠實標明屬防禦性加固、非修補已知繞過。
  2. requirecreateRequire 有禁令無 mutation(MEDIUM,採納):Grok 實測把兩字從禁用清單移除後既有 mutation 仍全綠——真實覆蓋缺口。補 2 條注入 mutation。
  3. LANGUAGE: 'C' 有 AST 檢查無 mutation(LOW,採納):對稱補 1 條弱化 mutation。
  4. vmworker_threadsprocess.binding(Open Question)→ 維持不防、書面出界:能力邊界註解明列不防類別(字串拼接/template literal/Reflect.getnode:vmworker_threadsprocess.binding),一律交由 code review 把關。

反向驗證:一次移除四道防線 → 恰好對應的 4 條新 mutation 全紅(4 failed|122 passed),還原後 126 全綠。

rebase 至 #886 後的 main(002 並行 PR 衝突收斂)

PR #886 併入後 main 的 002 累計 286 → 299,與本分支在同一插入點形成真實文字衝突。依 AGENTS.md「002 並行 PR 衝突解法」處理:22 個程式碼/文件 commit 與 #886 觸及檔案零相交、乾淨重放;002 聚合 commit 以 main 299 版為基底尾端重建——雙方條目時間序全保留、零刪除#886 的 13 筆在 base 側原位保留、本 PR 的 34 筆插頂),檔頭重算 299 + 20 = 319(delta +20 為本 PR 相對 merge-base 的淨變化,不受 rebase 影響)。

002(最終帳目)

> 本次分數變化:+20(reward 26、penalty 6、neutral 2)|累計總分:+319

34 筆條目集中於單一聚合 commit(全分支唯一動 002 的 commit,AGT-LOG-03 自身合規)。diff 相對 main:34 筆新增 ID、唯一刪除行為被取代的舊檔頭記分行、歷史條目零刪除零改寫。

CI 現況(headSha = d42c33512,rebase 後實測)

  • node scripts/verify-002-log.mjs --base-ref origin/main → exit 0;聚合 commit 的 pre-commit 逐 commit 語意同過
  • 品質閘:typecheck 9 apps PASS、pnpm test:root 126 passed、lint 0 error/9 既有 warning
  • gh pr checks 884Quality Checks pass(7m52s)、Lighthouse CI pass、E2E Smoke pass、CodeQL/Analyze×3 pass、SEO Audit pass、Dependency Review pass,零 fail(E2E Full 等為條件跳過)
  • mergeStateStatus: CLEAN

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

  • pnpm-lock.yaml

@github-actions

Copy link
Copy Markdown
Contributor

✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。

  • ✅ Sitemap 2026 標準
  • ✅ Breadcrumb Schema
  • ✅ JSON-LD 結構化數據
  • ✅ 內部連結結構

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa79a77b92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml
Comment thread scripts/verify-002-log.mjs
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- `git rm --cached docs/dev/002...` 時 index 已無該路徑,`git show :<path>` 回 null,
  原邏輯以「不在 staged set」靜默跳過 exit 0——整份刪除 002 可通過本地守門
- 改為區分「index 與 HEAD 皆無」(真正無關的 commit,跳過)與「HEAD 有但 index 已刪」
  (staged 刪除,必紅),與 CI base-ref 分支的刪除判定語意對齊
- 補 3 案 git 整合測試(staged 刪除必紅、兩邊皆無跳過、正常 append 綠燈),
  `runGuard` 改吃可變參數以同時驅動 pre-commit 與 --base-ref 兩種語意

測試:vitest 29 passed(拔掉刪除防護後該案轉紅、還原後綠);eslint scripts/ 0 error;
實跑 `git rm --cached` 情境 exit 1、正常 commit 情境 exit 0

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- `AGT-VER-01` 與 Phase 7 changeset 規範補上適用界線:changeset 對象是 package,
  變更落在 `apps/*/**`(含該 app 的 docs/README)要建立,純 root 工具與根文件則否
- 明載判斷依據為變更檔案所屬 package 而非 commit type,避免 `ci`/`chore` 動到 app
  卻漏 changeset、或純 root 變更硬補出無意義 CHANGELOG 條目
- `AGT-LOG-03` 補操作細節:002 落盤後的審查修正 commit 不得新增條目,
  補記須以 amend 或 rebase fold 併回同一 commit
- pre-commit 第 6 步描述補「staged 刪除整份 002 亦必紅」,與本次實作對齊

測試:prettier --check 通過;文件與 `.husky/pre-commit`、`ci.yml` 逐字對照無出入

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。

  • ✅ Sitemap 2026 標準
  • ✅ Breadcrumb Schema
  • ✅ JSON-LD 結構化數據
  • ✅ 內部連結結構

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3289dc911a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/verify-002-log.mjs Outdated
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- `isDirectRun` 改先 `realpathSync(process.argv[1])` 再與 `import.meta.url` 比對:
  macOS `/tmp` 為 `/private/tmp` symlink,以絕對路徑呼叫時兩側不等會使 `main()`
  靜默不執行卻 exit 0(假成功);兩個生產呼叫點走相對路徑不受影響,但手動除錯必得假陰性
- `parseEntries` 錯誤訊息改以條目 ID 為定位字串(取不到 ID 才退回首行):
  500+ 條目的檔案只引用日期行無法指出是哪一筆
- 修正解析器註解與 002 條目的破口規模數字:實跑複驗為 2 處黏合區塊(8 行 2 筆、
  12 行 3 筆)、3 筆隱形條目、納管 515→518;原述「5 處漏空行、6 筆隱形」高估——
  另 3 個超過 4 行的區塊實為單一條目帶 content_type/topics 額外欄位、非黏合
- 補 1 案 symlink 直跑回歸測試(顯式建 symlink,不依賴平台 /tmp 行為)

測試:vitest 30 passed(還原舊 isDirectRun 後該案轉紅、還原後綠);test:root 59 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 新增四行條目但 ID 寫成 `- ID:`(空值)時,`entry.id` 為空字串而不列格式錯誤;
  `newEntries` 以 truthy ID 篩選會排除該筆,使其同時繞過前綴、唯一性、計數與
  總分檢查——檔頭不動即全綠,等於憑空插入不計分的條目
- `parseEntries` 改以 trim 後的值判定,空白視同缺少 ID 並列出「條目 ID 不可為空」,
  與缺 ID 行分開措辭以利定位
- 補 2 案(空字串/僅空白);現存 002 於 df2033f/origin/main/HEAD 皆無空白 ID,
  不會回溯擋既有 commit

測試:vitest 32 passed(還原空白 ID 處理後 2 案轉紅、還原後綠);test:root 61 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c3426780b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/verify-002-log.mjs
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 條目行檢查只比對前綴,`- 原因:` 或 `- 解法:` 留空(含僅空白)仍全綠,
  使缺 root cause 或 resolution 的紀錄同時通過 pre-commit 與 CI
- 兩欄改取值後 trim 判空並列出「不可為空」;日期與 ID 各有專屬檢查故不重複
- 補 2 案;現存 002 於 df2033f/origin/main/HEAD 皆無空白內容欄位,不回溯擋既有 commit

測試:vitest 34 passed(拔掉非空檢查後 2 案轉紅、還原後綠);test:root 63 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f88ade5654

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/verify-002-log.mjs
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 已堵 `git rm` 整份刪除,但「保留檔案與 ID、把內容清空」是等效的規避路徑,
  實質同樣湮滅 penalty 證據;日期/原因/解法基準版非空者改後不得為空
  (含刪整行、留空值、只剩空白),ID 被掏空則由既有的刪除防護攔下
- 判準只看「有沒有從有變成無」而非內容是否改動:改寫與合法的精確性修正
  無法機械區分,擋下會封死唯一的更正管道,故維持不擋、交由審查把關
- `parseEntries` 抽出 fields map(各欄 trim 後取值),供內容非空檢查與跨版本比對共用
- 不採用「回溯驗證既有條目四行完整性」:main 有 218 筆無標準前綴、3 筆帶額外欄位,
  全面回溯會對現存資料直接報錯,而修正歷史又觸犯不可刪改原則
- 補 4 案(日期/原因/解法掏空必紅;正向對照採本 PR 真實的數字更正 diff 必綠)

測試:vitest 38 passed(拔掉掏空防護後 3 案轉紅、還原後綠);test:root 67 passed;
實跑三組真實 diff(我的數字更正、本分支全體、main 自身推進)皆無誤判;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- Codex 描述的具體情境為「既有四行條目縮成只剩日期與 ID」(整行刪除而非留空值),
  與留空值同屬掏空,需確認不因「歷史條目格式錯誤不回溯」被略過
- 補 1 案鎖住整行刪除路徑;實測 fields map 對缺行回傳 undefined,
  與基準版非空比對後同樣列出兩條「原有內容不可清空」

測試:vitest 39 passed;test:root 68 passed;eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b70a822e15

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .husky/pre-commit Outdated
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- pre-commit 第 6 步改無條件執行:以 `git diff --cached --name-only` 判斷觸發會被
  `git mv` 繞過(只列新路徑、舊路徑不出現),守門不應依賴 diff 呈現方式;
  跳過與否交由腳本以 index/HEAD 存在性決定,002 未變更時約 50ms
- ci.yml 補 main push 事件的守門,基準取 `github.event.before`(可涵蓋一次推多個
  commit,merge commit 的 before 即第一父);全零 SHA 情境跳過而非誤紅
- 自主排查另找到第五條繞過路徑並修掉:多個「## 條目」區段時只解析第一個,
  前置 decoy 抄齊全部 ID 即滿足刪除防護、真區段從此不受檢視——改為區段必須唯一
- 補 4 案回歸鎖:git mv 必紅、decoy 區段必紅,以及 hook 觸發條件與 CI 條件
  兩個「觸發面」的結構鎖(前者是這兩個洞的實際所在位置)
- 文件記錄 merge commit 走 pre-merge-commit 而非 pre-commit 的已知缺口與
  刻意不補的理由(會讓 git merge origin/main 誤紅),由 CI 兜底

測試:vitest 43 passed(hook 條件、CI 條件、decoy 三項各自紅綠驗證);test:root 72 passed;
10 條繞過路徑排查全數擋下;真實 002 三個參照點零誤判;eslint scripts/ 0 error

Co-authored-by: Cursor <cursoragent@cursor.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d5a673224

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/verify-002-log.mjs Outdated
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- `git rm --cached docs/dev/002...` 時 index 已無該路徑,`git show :<path>` 回 null,
  原邏輯以「不在 staged set」靜默跳過 exit 0——整份刪除 002 可通過本地守門
- 改為區分「index 與 HEAD 皆無」(真正無關的 commit,跳過)與「HEAD 有但 index 已刪」
  (staged 刪除,必紅),與 CI base-ref 分支的刪除判定語意對齊
- 補 3 案 git 整合測試(staged 刪除必紅、兩邊皆無跳過、正常 append 綠燈),
  `runGuard` 改吃可變參數以同時驅動 pre-commit 與 --base-ref 兩種語意

測試:vitest 29 passed(拔掉刪除防護後該案轉紅、還原後綠);eslint scripts/ 0 error;
實跑 `git rm --cached` 情境 exit 1、正常 commit 情境 exit 0

Co-authored-by: Cursor <cursoragent@cursor.com>
@s123104
s123104 force-pushed the ci/starpuff-002-log-gate branch from 2d5a673 to 814da08 Compare July 26, 2026 11:12
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- `AGT-VER-01` 與 Phase 7 changeset 規範補上適用界線:changeset 對象是 package,
  變更落在 `apps/*/**`(含該 app 的 docs/README)要建立,純 root 工具與根文件則否
- 明載判斷依據為變更檔案所屬 package 而非 commit type,避免 `ci`/`chore` 動到 app
  卻漏 changeset、或純 root 變更硬補出無意義 CHANGELOG 條目
- `AGT-LOG-03` 補操作細節:002 落盤後的審查修正 commit 不得新增條目,
  補記須以 amend 或 rebase fold 併回同一 commit
- pre-commit 第 6 步描述補「staged 刪除整份 002 亦必紅」,與本次實作對齊

測試:prettier --check 通過;文件與 `.husky/pre-commit`、`ci.yml` 逐字對照無出入

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- `isDirectRun` 改先 `realpathSync(process.argv[1])` 再與 `import.meta.url` 比對:
  macOS `/tmp` 為 `/private/tmp` symlink,以絕對路徑呼叫時兩側不等會使 `main()`
  靜默不執行卻 exit 0(假成功);兩個生產呼叫點走相對路徑不受影響,但手動除錯必得假陰性
- `parseEntries` 錯誤訊息改以條目 ID 為定位字串(取不到 ID 才退回首行):
  500+ 條目的檔案只引用日期行無法指出是哪一筆
- 修正解析器註解與 002 條目的破口規模數字:實跑複驗為 2 處黏合區塊(8 行 2 筆、
  12 行 3 筆)、3 筆隱形條目、納管 515→518;原述「5 處漏空行、6 筆隱形」高估——
  另 3 個超過 4 行的區塊實為單一條目帶 content_type/topics 額外欄位、非黏合
- 補 1 案 symlink 直跑回歸測試(顯式建 symlink,不依賴平台 /tmp 行為)

測試:vitest 30 passed(還原舊 isDirectRun 後該案轉紅、還原後綠);test:root 59 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 新增四行條目但 ID 寫成 `- ID:`(空值)時,`entry.id` 為空字串而不列格式錯誤;
  `newEntries` 以 truthy ID 篩選會排除該筆,使其同時繞過前綴、唯一性、計數與
  總分檢查——檔頭不動即全綠,等於憑空插入不計分的條目
- `parseEntries` 改以 trim 後的值判定,空白視同缺少 ID 並列出「條目 ID 不可為空」,
  與缺 ID 行分開措辭以利定位
- 補 2 案(空字串/僅空白);現存 002 於 df2033f/origin/main/HEAD 皆無空白 ID,
  不會回溯擋既有 commit

測試:vitest 32 passed(還原空白 ID 處理後 2 案轉紅、還原後綠);test:root 61 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 條目行檢查只比對前綴,`- 原因:` 或 `- 解法:` 留空(含僅空白)仍全綠,
  使缺 root cause 或 resolution 的紀錄同時通過 pre-commit 與 CI
- 兩欄改取值後 trim 判空並列出「不可為空」;日期與 ID 各有專屬檢查故不重複
- 補 2 案;現存 002 於 df2033f/origin/main/HEAD 皆無空白內容欄位,不回溯擋既有 commit

測試:vitest 34 passed(拔掉非空檢查後 2 案轉紅、還原後綠);test:root 63 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 已堵 `git rm` 整份刪除,但「保留檔案與 ID、把內容清空」是等效的規避路徑,
  實質同樣湮滅 penalty 證據;日期/原因/解法基準版非空者改後不得為空
  (含刪整行、留空值、只剩空白),ID 被掏空則由既有的刪除防護攔下
- 判準只看「有沒有從有變成無」而非內容是否改動:改寫與合法的精確性修正
  無法機械區分,擋下會封死唯一的更正管道,故維持不擋、交由審查把關
- `parseEntries` 抽出 fields map(各欄 trim 後取值),供內容非空檢查與跨版本比對共用
- 不採用「回溯驗證既有條目四行完整性」:main 有 218 筆無標準前綴、3 筆帶額外欄位,
  全面回溯會對現存資料直接報錯,而修正歷史又觸犯不可刪改原則
- 補 4 案(日期/原因/解法掏空必紅;正向對照採本 PR 真實的數字更正 diff 必綠)

測試:vitest 38 passed(拔掉掏空防護後 3 案轉紅、還原後綠);test:root 67 passed;
實跑三組真實 diff(我的數字更正、本分支全體、main 自身推進)皆無誤判;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- Codex 描述的具體情境為「既有四行條目縮成只剩日期與 ID」(整行刪除而非留空值),
  與留空值同屬掏空,需確認不因「歷史條目格式錯誤不回溯」被略過
- 補 1 案鎖住整行刪除路徑;實測 fields map 對缺行回傳 undefined,
  與基準版非空比對後同樣列出兩條「原有內容不可清空」

測試:vitest 39 passed;test:root 68 passed;eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 第十二破口:工作區有檔、tree 無檔時 `cat-file -e` 回「exists on disk, but not in
  '<ref>'」,與已涵蓋的 index 版訊息不同,四條 pattern 全不匹配而 throw;
  「已有 commit 的 repo 首次引入 002」這個合法情境在 pre-commit 與 CI 都被誤擋
- 未採「再加第五條 pattern」:訊息比對已連續破三次(漏訊息種類、依賴英文輸出、
  又漏一種),根因是把 git 的人類可讀 fatal 當成 API 契約,加 pattern 只是打地鼠
- 改為結構化探測:先 `rev-parse --git-dir` 確認 repo 可用(僅一次),之後 index 用
  `ls-files`、tree 用 `ls-tree --name-only`——不存在時輸出空字串且 exit 0,
  把「不存在」從例外變成正常回傳值;非零離開一律環境問題並 fail-closed。
  「尚無 commit」由 `rev-parse --verify` 離開碼結構判定。零訊息比對
- 實測六個「不存在」情境有五個變成 exit 0 空輸出,僅「尚無 commit」需另判
- 所有 git 子行程收斂在唯一的 `git()` wrapper 帶 GIT_ENV;結構鎖由「逐一檢查每個
  呼叫點是否帶 env」(字串偵測、可用雙引號/spawnSync/間接呼叫繞過)改為
  「全檔只允許一個子行程呼叫點且必須在 wrapper 內」,讓忘記帶 env 不可能發生
- 另補結構鎖禁止退回訊息比對;`invalid object name 'HEAD'\.` 的句點錨定問題隨
  pattern 整體移除而消失

測試:vitest 76 passed(兩個新情境+兩道結構鎖各自紅綠驗證——還原成 cat-file
實作後 4 案轉紅);test:root 105 passed;三種 git 環境失敗維持 fail-closed;
10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
s123104 pushed a commit that referenced this pull request Jul 26, 2026
- 第十三破口(CRITICAL):方法層置換只覆蓋 path 存在性,「無基準版」仍綁在
  「HEAD 不是 commit ⇒ null」。`git checkout --orphan`(標準指令、不需劫持環境)
  之後 rev-parse --verify 失敗但歷史 commit 都在,守門視為無基準版而跳過刪除防護
  與總分鏈——實測掏空 002 後 exit 0,penalty 條目消失
- 改由 `git rev-list -n 1 --all` 是否為空來證明「無基準版」:只有真的一個 commit
  都沒有才回 null,ref 不解析但 repo 已有 commit(orphan/損毀 symref/基準 ref
  失效)一律 throw fail-closed。未加任何訊息 pattern
- 移除 :342-349 與新做法直接矛盾的過時註解(宣稱「必須靠 stderr 區分」「窮舉四種
  合法不存在訊息」),並修正 `refResolves`/`assertRepoUsable` 已被 orphan 證偽的
  保證敘述
- 措辭鎖改 regex:字面比對曾漏接同一主張的改寫版(關鍵詞間插入修飾語即躲過),
  改以主張骨架比對並補 3 條;同時在測試註解與文件寫明此鎖只擋已知舊主張、
  無法偵測全新錯誤敘述——真正的防線是鎖行為的結構測試
- 結構鎖升級為 AST(typescript compiler):限制 child_process 只能具名未改名匯入
  (禁 namespace/default,因 `cp['execFileSync'](` 無法靠名稱追蹤),再確認所有
  呼叫落在 git() wrapper 內。舊字串鎖對 `import { execFileSync as run }` 與
  wrapper 外新增呼叫皆漏接,兩者現在都必紅

測試:vitest 78 passed(orphan 掏空必紅、真空 repo 仍放行、AST 鎖兩種繞法、
措辭鎖改寫版各自紅綠驗證);test:root 107 passed;三種 git 環境失敗維持 fail-closed;
10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。

  • ✅ Sitemap 2026 標準
  • ✅ Breadcrumb Schema
  • ✅ JSON-LD 結構化數據
  • ✅ 內部連結結構

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。

  • ✅ Sitemap 2026 標準
  • ✅ Breadcrumb Schema
  • ✅ JSON-LD 結構化數據
  • ✅ 內部連結結構

haotool and others added 23 commits July 27, 2026 05:55
- 移植 scripts/verify-002-log.mjs:檔頭記分行與新增條目對帳(a+b+c=新增數、N=a-b)、
  累計總分鏈(前版 + N)、條目四行模板、ID 全檔唯一、歷史條目不可刪除
- 掛入 .husky/pre-commit 第 6 步(僅 002 在 staged set 時執行),對齊 main 現況由 5 步升為 6 步
- ci.yml quality job 於 install 前加 PR 專屬 --base-ref 步驟:以 merge-base 驗 PR 最終態,
  堵 --no-verify 與網頁端 squash 繞過 pre-commit 的破口
- 解析器補「- 日期:」邊界:歷史檔 2 處漏空行(8 行 2 筆、12 行 3 筆)使 3 筆條目對
  唯一性與刪除防護隱形,修正後 515→518 筆全數納管(含 2026-07-20/07-22 兩筆近期條目)
- 26 例單元測試掛入 test:root;root 補 @types/node 使 scripts 測試可用 node 內建模組型別,
  lockfile 的 @types/node peer 由漂移的 26.1.1 收斂為 24.13.3(對齊 engines ^24 與各 app)

測試:vitest 26 passed;test:root 55 passed;pnpm lint 0 error;pnpm typecheck 全過;
對 main 現存 518 條目實跑守門通過;七種故障注入全數 exit 1

Co-authored-by: Cursor <cursoragent@cursor.com>
- `AGENTS.md`/`CLAUDE.md` 補齊 pre-commit 6 步驟、檔頭記分行固定格式與 rebase 例外說明,
  控制矩陣 `AGT-PC-01` 由 5 步改 6 步
- 新增 CI 端 002 守門章節(位置/指令/merge-base 語意/定位),來源分支從未寫入此段
- 新增控制項 `AGT-LOG-03`:一個 PR 的 002 條目必須集中在單一 commit、檔頭寫聚合淨變化,
  並說明 pre-commit 逐 commit 語意與 CI 聚合語意為何互斥
- 002 補 3 筆 reward(守門移植、黏塊解析修正、單一提交約束實證),累計總分 +251 → +254

測試:pre-commit 六步全過(Step 6/6 守門對本次 3 筆新增條目綠燈);prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
- `git rm --cached docs/dev/002...` 時 index 已無該路徑,`git show :<path>` 回 null,
  原邏輯以「不在 staged set」靜默跳過 exit 0——整份刪除 002 可通過本地守門
- 改為區分「index 與 HEAD 皆無」(真正無關的 commit,跳過)與「HEAD 有但 index 已刪」
  (staged 刪除,必紅),與 CI base-ref 分支的刪除判定語意對齊
- 補 3 案 git 整合測試(staged 刪除必紅、兩邊皆無跳過、正常 append 綠燈),
  `runGuard` 改吃可變參數以同時驅動 pre-commit 與 --base-ref 兩種語意

測試:vitest 29 passed(拔掉刪除防護後該案轉紅、還原後綠);eslint scripts/ 0 error;
實跑 `git rm --cached` 情境 exit 1、正常 commit 情境 exit 0

Co-authored-by: Cursor <cursoragent@cursor.com>
- `AGT-VER-01` 與 Phase 7 changeset 規範補上適用界線:changeset 對象是 package,
  變更落在 `apps/*/**`(含該 app 的 docs/README)要建立,純 root 工具與根文件則否
- 明載判斷依據為變更檔案所屬 package 而非 commit type,避免 `ci`/`chore` 動到 app
  卻漏 changeset、或純 root 變更硬補出無意義 CHANGELOG 條目
- `AGT-LOG-03` 補操作細節:002 落盤後的審查修正 commit 不得新增條目,
  補記須以 amend 或 rebase fold 併回同一 commit
- pre-commit 第 6 步描述補「staged 刪除整份 002 亦必紅」,與本次實作對齊

測試:prettier --check 通過;文件與 `.husky/pre-commit`、`ci.yml` 逐字對照無出入

Co-authored-by: Cursor <cursoragent@cursor.com>
- `isDirectRun` 改先 `realpathSync(process.argv[1])` 再與 `import.meta.url` 比對:
  macOS `/tmp` 為 `/private/tmp` symlink,以絕對路徑呼叫時兩側不等會使 `main()`
  靜默不執行卻 exit 0(假成功);兩個生產呼叫點走相對路徑不受影響,但手動除錯必得假陰性
- `parseEntries` 錯誤訊息改以條目 ID 為定位字串(取不到 ID 才退回首行):
  500+ 條目的檔案只引用日期行無法指出是哪一筆
- 修正解析器註解與 002 條目的破口規模數字:實跑複驗為 2 處黏合區塊(8 行 2 筆、
  12 行 3 筆)、3 筆隱形條目、納管 515→518;原述「5 處漏空行、6 筆隱形」高估——
  另 3 個超過 4 行的區塊實為單一條目帶 content_type/topics 額外欄位、非黏合
- 補 1 案 symlink 直跑回歸測試(顯式建 symlink,不依賴平台 /tmp 行為)

測試:vitest 30 passed(還原舊 isDirectRun 後該案轉紅、還原後綠);test:root 59 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
- 新增四行條目但 ID 寫成 `- ID:`(空值)時,`entry.id` 為空字串而不列格式錯誤;
  `newEntries` 以 truthy ID 篩選會排除該筆,使其同時繞過前綴、唯一性、計數與
  總分檢查——檔頭不動即全綠,等於憑空插入不計分的條目
- `parseEntries` 改以 trim 後的值判定,空白視同缺少 ID 並列出「條目 ID 不可為空」,
  與缺 ID 行分開措辭以利定位
- 補 2 案(空字串/僅空白);現存 002 於 df2033f/origin/main/HEAD 皆無空白 ID,
  不會回溯擋既有 commit

測試:vitest 32 passed(還原空白 ID 處理後 2 案轉紅、還原後綠);test:root 61 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
- 條目行檢查只比對前綴,`- 原因:` 或 `- 解法:` 留空(含僅空白)仍全綠,
  使缺 root cause 或 resolution 的紀錄同時通過 pre-commit 與 CI
- 兩欄改取值後 trim 判空並列出「不可為空」;日期與 ID 各有專屬檢查故不重複
- 補 2 案;現存 002 於 df2033f/origin/main/HEAD 皆無空白內容欄位,不回溯擋既有 commit

測試:vitest 34 passed(拔掉非空檢查後 2 案轉紅、還原後綠);test:root 63 passed;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
- 已堵 `git rm` 整份刪除,但「保留檔案與 ID、把內容清空」是等效的規避路徑,
  實質同樣湮滅 penalty 證據;日期/原因/解法基準版非空者改後不得為空
  (含刪整行、留空值、只剩空白),ID 被掏空則由既有的刪除防護攔下
- 判準只看「有沒有從有變成無」而非內容是否改動:改寫與合法的精確性修正
  無法機械區分,擋下會封死唯一的更正管道,故維持不擋、交由審查把關
- `parseEntries` 抽出 fields map(各欄 trim 後取值),供內容非空檢查與跨版本比對共用
- 不採用「回溯驗證既有條目四行完整性」:main 有 218 筆無標準前綴、3 筆帶額外欄位,
  全面回溯會對現存資料直接報錯,而修正歷史又觸犯不可刪改原則
- 補 4 案(日期/原因/解法掏空必紅;正向對照採本 PR 真實的數字更正 diff 必綠)

測試:vitest 38 passed(拔掉掏空防護後 3 案轉紅、還原後綠);test:root 67 passed;
實跑三組真實 diff(我的數字更正、本分支全體、main 自身推進)皆無誤判;
eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
- Codex 描述的具體情境為「既有四行條目縮成只剩日期與 ID」(整行刪除而非留空值),
  與留空值同屬掏空,需確認不因「歷史條目格式錯誤不回溯」被略過
- 補 1 案鎖住整行刪除路徑;實測 fields map 對缺行回傳 undefined,
  與基準版非空比對後同樣列出兩條「原有內容不可清空」

測試:vitest 39 passed;test:root 68 passed;eslint scripts/ 0 error;prettier --check 通過

Co-authored-by: Cursor <cursoragent@cursor.com>
- pre-commit 第 6 步改無條件執行:以 `git diff --cached --name-only` 判斷觸發會被
  `git mv` 繞過(只列新路徑、舊路徑不出現),守門不應依賴 diff 呈現方式;
  跳過與否交由腳本以 index/HEAD 存在性決定,002 未變更時約 50ms
- ci.yml 補 main push 事件的守門,基準取 `github.event.before`(可涵蓋一次推多個
  commit,merge commit 的 before 即第一父);全零 SHA 情境跳過而非誤紅
- 自主排查另找到第五條繞過路徑並修掉:多個「## 條目」區段時只解析第一個,
  前置 decoy 抄齊全部 ID 即滿足刪除防護、真區段從此不受檢視——改為區段必須唯一
- 補 4 案回歸鎖:git mv 必紅、decoy 區段必紅,以及 hook 觸發條件與 CI 條件
  兩個「觸發面」的結構鎖(前者是這兩個洞的實際所在位置)
- 文件記錄 merge commit 走 pre-merge-commit 而非 pre-commit 的已知缺口與
  刻意不補的理由(會讓 git merge origin/main 誤紅),由 CI 兜底

測試:vitest 43 passed(hook 條件、CI 條件、decoy 三項各自紅綠驗證);test:root 72 passed;
10 條繞過路徑排查全數擋下;真實 002 三個參照點零誤判;eslint scripts/ 0 error

Co-authored-by: Cursor <cursoragent@cursor.com>
- main push 兜底原用 `--base-ref` 走 merge-base,但 force push 時 `before` 並非 HEAD
  的祖先,基準會退到更早的共同祖先——被改寫掉的 penalty 條目因此驗不出來,
  而那正是此模式的存在理由;新增 `--base-commit` 直取該 commit,ci.yml 改用之
- 檔頭嚴格解析的 `[+-]?` 讓「本次分數變化:1」「累計總分:171」通過,與文件所定的
  固定 `+N`/`+T` 不符;改為強制顯式符號(負值仍可解析)
- `CLAUDE.md` 仍寫第 6 步「僅 002 檔變更時執行」,與已改為無條件執行的 hook 相斥——
  維護者依該句調整 hook 會重新引入 `git mv` 繞過缺口,同步修正
- AGENTS.md 明載兩種模式基準取法不同且不可混用
- 補 5 案回歸鎖(force push 改寫必紅、基準無法解析必紅、缺正號必失敗、負值可解析、
  ci.yml 禁止 push 步驟使用 --base-ref)

測試:vitest 48 passed(三項各自紅綠驗證);test:root 77 passed;eslint scripts/ 0 error;
兩種 CI 模式對 main 0851528 實跑皆通過;10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
- CASCADE(Blocking):`parseEntries(headContent)` 只取 entries 丟棄 globalErrors,
  基準版本身不可解析時 headEntryIds 為空集合使刪除檢查落入真空——「先讓 tip 變成
  不可解析、下一個 commit 清空全部歷史」兩道閘全綠。雙保險修法:基準版有 globalErrors
  即 fail-closed;刪除比對另取「解析結果 ∪ 原始文字掃描 `- ID:`」不依賴解析成功
- 上述 raw 掃描初版讓「插入 `## ` 標題截斷」「條目搬到區段之前」兩條既有防線失效
  (被隱藏的條目仍出現在 raw 集合故不算刪除);繞過路徑排查腳本當場抓到,補
  hiddenIds 檢查——基準版解析得到、待驗版只剩原始文字者一律擋下
- 同族第八條:基準版讀不出累計總分時原本靜默跳過總分鏈,可寫入任意總分;
  改為僅「無基準版」(初始 commit)才跳過,讀不出即失敗
- `--base-ref` 與 `--base-commit` 改互斥;原實作迴圈先匹配 --base-ref 使誤用得到假綠
- 文件逐句對照實作掃描共修 12 處:AGT-LOG-01 與 AGT-LOG-03 字面衝突、Phase 4 步驟 1、
  AGENTS.md 記分段、CLAUDE.md Execution SOP 與第 107/109/113 行舊語意、
  hook 開銷由「約 50ms」更正為實測 p50 120–160ms(n=30)、模組頂部註解補三模式
- 新增 `AGT-LOG-04` 把「語意掏空/內容對調」明列為殘餘風險與人工審查責任
- 新增「失敗行為契約」表:列出全部 fail-closed 情境與僅有的三種放行情境

測試:vitest 55 passed(CASCADE 兩例+E2E、總分鏈 fail-closed、雙 flag 互斥、
移出解析範圍兩例各自紅綠驗證);10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
- `gitShow` 把所有 git 失敗都當成「物件不存在」回 null:實測 PATH 移除 git 後整道守門
  靜默 exit 0;改為先 `git cat-file -e` 判存在性,存在卻讀不出即向上拋,
  並於直跑入口統一捕捉為 fail-closed
- ID 唯一性原本無條件掃全檔:歷史一旦出現重複,之後每個 commit 都會被卡死(即使沒動
  002)。改為只對「本次造成的重複」擋,比照格式檢查的歷史不回溯
- 刪除比對原本聯集全檔原始文字掃描,使區段外的獨立 `- ID:` 行(文件範例)被移除時
  誤報刪除歷史條目;改為基準版可解析時只採信解析結果,不可解析時才退回全檔掃描
- `headContent` 判斷統一為 `!= null`;空字串基準改走解析 fail-closed 而非「無基準」
- 全 repo grep 掃描(含隱藏目錄)找出 6 個描述本守門的檔案,逐句對齊實作共修 8 處:
  腳本檔頭仍寫「基準版無法解析時跳過」、`.husky/pre-commit` 與 `ci.yml` 註解的開銷/
  「毫秒級」表述、AGENTS.md 與 CLAUDE.md 的唯一性與刪除比對範圍
- 開銷改為不記具體數字:四方觀測跨越一個數量級(p50 54~148ms、曾見 max 784ms),
  只保留「遠低於同一 hook 內的 pnpm typecheck」這個機器無關的相對比較
- 殘餘風險移出控制矩陣改列獨立小節,避免「有編號=有控制」的視覺誤判

測試:vitest 60 passed(git 不可執行、區段外 ID 行、歷史重複不回溯、新增重複必紅、
空字串基準各自紅綠驗證);test:root 89 passed;10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
- 第十破口:`gitShow` 對非 ENOENT 的錯誤一律 return null,而 git 用同一個 status 128
  同時表示「路徑不存在」與「repo 不可用」——實測壞 git stub(status 1)、GIT_DIR 無效、
  `.git` chmod 000 三種環境失敗都讓守門 exit 0;CI 因 merge-base 先失敗而倖免,
  但 SOP 要求 rebase 後手動直跑 pre-commit 模式,正是受害路徑
- 改以 stderr 區分:窮舉實測四種合法「不存在」訊息(tree 無此路徑/index 與磁碟皆無/
  exists on disk but not in the index/invalid object name 'HEAD'),其餘一律上拋。
  Grok 建議的 regex 只含 does not exist,會誤擋「尚無 commit」的初始 commit 路徑
- `catch` 逐一稽核:`isDirectRun` 原本吞掉 realpath 例外並退回字面比較,比不中即靜默
  不執行 main() 而 exit 0;改為不吞、由統一入口 fail-closed。merge-base 與 rev-parse
  兩處雖已 exit 1 但丟棄 git 原始訊息會誤導診斷,改為帶出 stderr
- `headHeaderLine` 的 `headContent ?` 對齊 `hasBase`
- 文件矛盾:`AGENTS.md:217`/`CLAUDE.md:109` 仍寫「不依賴解析是否成功」與實作相反;
  擴充關鍵字含被取代的舊措辭後另掃出腳本 `:204` 同樣問題(Grok 未點名),共修 4 處
- 漂移預防改為機械強制:固定範圍 × `SUPERSEDED_PHRASES` 清單由測試把關,
  不再要求下一任判斷「什麼算涉及守門行為」

測試:vitest 70 passed(壞 stub/GIT_DIR/.git 權限三案+isDirectRun 結構鎖+
措辭漂移檢查各自紅綠驗證);test:root 99 passed;10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
- `gitShow` 的四種「物件不存在」判別讀 stderr 文字,但 git 內建 gettext 翻譯會跟隨
  呼叫端 locale,`execFileSync` 未傳 env 即繼承。實測 macOS Homebrew git 2.55 +
  `LC_ALL=zh_CN.UTF-8`:`致命错误:无效的对象名 'HEAD'。` 不 match 任何 pattern,
  使 AGENTS.md 明載支援的「初始 commit」情境由 exit 0 變 exit 1
- 方向是 fail-closed 非 fail-open,不是資安退化,但會讓合法情境誤擋;根因是
  「窮舉完整」這個宣稱的隱含前提(英文輸出)未被驗證也未被文件化
- 四處 git 呼叫(cat-file/show/merge-base/rev-parse)統一傳
  `env: { ...process.env, LC_ALL: 'C', LANGUAGE: 'C' }`;鎖 C locale 同時鎖住訊息
  穩定性,降低跨 git 版本翻譯字串變動的風險(審查席標為低信心殘留的那項)
- 補 3 案回歸:結構鎖要求四處呼叫都帶 env(CI 上真正有效的那道,新增第五處會紅)、
  非英文 locale 下「尚無 commit」仍跳過、staged 刪除仍必紅
- AGENTS.md/CLAUDE.md 補「依賴 git 文字輸出的前提」與新增呼叫須帶 env 的要求

測試:vitest 73 passed(拿掉任一處 env 即紅);test:root 102 passed;
七情境 × C.UTF-8/zh_CN.UTF-8 雙 locale 實跑行為完全一致

Co-authored-by: Cursor <cursoragent@cursor.com>
- 第十二破口:工作區有檔、tree 無檔時 `cat-file -e` 回「exists on disk, but not in
  '<ref>'」,與已涵蓋的 index 版訊息不同,四條 pattern 全不匹配而 throw;
  「已有 commit 的 repo 首次引入 002」這個合法情境在 pre-commit 與 CI 都被誤擋
- 未採「再加第五條 pattern」:訊息比對已連續破三次(漏訊息種類、依賴英文輸出、
  又漏一種),根因是把 git 的人類可讀 fatal 當成 API 契約,加 pattern 只是打地鼠
- 改為結構化探測:先 `rev-parse --git-dir` 確認 repo 可用(僅一次),之後 index 用
  `ls-files`、tree 用 `ls-tree --name-only`——不存在時輸出空字串且 exit 0,
  把「不存在」從例外變成正常回傳值;非零離開一律環境問題並 fail-closed。
  「尚無 commit」由 `rev-parse --verify` 離開碼結構判定。零訊息比對
- 實測六個「不存在」情境有五個變成 exit 0 空輸出,僅「尚無 commit」需另判
- 所有 git 子行程收斂在唯一的 `git()` wrapper 帶 GIT_ENV;結構鎖由「逐一檢查每個
  呼叫點是否帶 env」(字串偵測、可用雙引號/spawnSync/間接呼叫繞過)改為
  「全檔只允許一個子行程呼叫點且必須在 wrapper 內」,讓忘記帶 env 不可能發生
- 另補結構鎖禁止退回訊息比對;`invalid object name 'HEAD'\.` 的句點錨定問題隨
  pattern 整體移除而消失

測試:vitest 76 passed(兩個新情境+兩道結構鎖各自紅綠驗證——還原成 cat-file
實作後 4 案轉紅);test:root 105 passed;三種 git 環境失敗維持 fail-closed;
10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
- 第十三破口(CRITICAL):方法層置換只覆蓋 path 存在性,「無基準版」仍綁在
  「HEAD 不是 commit ⇒ null」。`git checkout --orphan`(標準指令、不需劫持環境)
  之後 rev-parse --verify 失敗但歷史 commit 都在,守門視為無基準版而跳過刪除防護
  與總分鏈——實測掏空 002 後 exit 0,penalty 條目消失
- 改由 `git rev-list -n 1 --all` 是否為空來證明「無基準版」:只有真的一個 commit
  都沒有才回 null,ref 不解析但 repo 已有 commit(orphan/損毀 symref/基準 ref
  失效)一律 throw fail-closed。未加任何訊息 pattern
- 移除 :342-349 與新做法直接矛盾的過時註解(宣稱「必須靠 stderr 區分」「窮舉四種
  合法不存在訊息」),並修正 `refResolves`/`assertRepoUsable` 已被 orphan 證偽的
  保證敘述
- 措辭鎖改 regex:字面比對曾漏接同一主張的改寫版(關鍵詞間插入修飾語即躲過),
  改以主張骨架比對並補 3 條;同時在測試註解與文件寫明此鎖只擋已知舊主張、
  無法偵測全新錯誤敘述——真正的防線是鎖行為的結構測試
- 結構鎖升級為 AST(typescript compiler):限制 child_process 只能具名未改名匯入
  (禁 namespace/default,因 `cp['execFileSync'](` 無法靠名稱追蹤),再確認所有
  呼叫落在 git() wrapper 內。舊字串鎖對 `import { execFileSync as run }` 與
  wrapper 外新增呼叫皆漏接,兩者現在都必紅

測試:vitest 78 passed(orphan 掏空必紅、真空 repo 仍放行、AST 鎖兩種繞法、
措辭鎖改寫版各自紅綠驗證);test:root 107 passed;三種 git 環境失敗維持 fail-closed;
10 條繞過路徑排查全數擋下

Co-authored-by: Cursor <cursoragent@cursor.com>
- 開工活性錨,本輪處理 Grok 收官確認的 5 條跟進項

測試:本 commit 無程式碼變更,未執行測試

Co-authored-by: Cursor <cursoragent@cursor.com>
- hasAnyCommit 由 rev-list --all 升級為 --all --reflog + object store 探測
- orphan+刪光 named refs(含再 expire reflog)掏空 002 由假綠 exit 0 改為必紅
- 真空 repo(零 commit)維持放行;commit 物件被 prune 的極端情境本機無法區分
- 上述殘留登記 AGENTS.md 殘餘風險表,由 CI 基準 SHA 兜底
- AGENTS.md/CLAUDE.md 無基準判準敘述同步為與實作同構
- 結構鎖禁字由 cat-file 精確化為 cat-file -e(--batch-check 為合法用途)
- 新增 reflog 探測與 object store 探測兩個整合測試

測試:pnpm test:root 109 passed(含 2 新增);/tmp 臨時 repo 實證修前假綠、修後必紅

Co-authored-by: Cursor <cursoragent@cursor.com>
- AST 檢查抽為 auditGitWrapperStructure 單一函數,真實檔案斷言零違規
- 堵變數別名:execFileSync 識別字只能作 import 綁定或直接呼叫 callee
- 堵動態產碼/取模組:eval、Function、getBuiltinModule、require、createRequire 識別字全檔禁止
- 堵動態 import 與字串夾帶:字串字面量不得含子行程 API 名(import 來源除外)
- env 檢查升級 AST:options 須有 PropertyAssignment env 指向 GIT_ENV,註解字面不再假陽性
- GIT_ENV 定義本身也走 AST(展開 process.env、LC_ALL/LANGUAGE 為 C)
- 12 條 mutation 元測試釘住每條繞法必紅,防守門自身退化

測試:pnpm test:root 121 passed(109 → 121,新增 12 條 mutation)

Co-authored-by: Cursor <cursoragent@cursor.com>
- SUPERSEDED_PHRASES 舊常數名漂移修正(AGENTS.md),並把舊名加進措辭鎖自我偵測
- 措辭鎖補舊「無基準」判準 pattern(rev-list --all 即結尾者),防敘述回退
- merge-base exit 1(documented 無共同祖先語意)給專用診斷訊息,維持 fail-closed
- orphan PR 走 --base-ref 的診斷測試+AGENTS.md 失敗行為契約表同步
- 腳本與測試註解的層次表述對齊為 refs/reflog/object store 三層

測試:pnpm test:root 122 passed;/tmp orphan PR repo 實證 before 訊息空尾巴、after 專用訊息且皆 exit 1

Co-authored-by: Cursor <cursoragent@cursor.com>
- 補 require/createRequire 注入與 LANGUAGE 弱化 3 條 mutation(禁令原已有效,元測試補防退化)
- specifier 匹配放寬為路徑段含 child_process 者皆檢(防禦性加固,該子路徑現行 Node 不存在)
- 帶子路徑 specifier 補 1 條 mutation 釘住加固範圍
- 能力邊界書面出界:字串拼接/template literal/Reflect.get/vm/worker_threads/process.binding 交 code review

測試:pnpm test:root 126 passed(122→126);反向驗證移除四道防線時恰好對應 4 條 mutation 全紅

Co-authored-by: Cursor <cursoragent@cursor.com>
- 收斂 AGT-LOG-03 單一 002 commit:檔頭反映 PR 對 main 的聚合淨變化
- 34 筆條目 = 守門移植攻防+審查收斂+#857 事故+收斂輪強化+複審補洞
- rebase 至 #886 後的 main:雙方條目時間序全保留零刪除,基準累計 299
- 檔頭聚合:+20(reward 26、penalty 6、neutral 2)|累計 +319(299+20)

測試:verify-002-log.mjs pre-commit 語意與 --base-ref origin/main CI 語意皆 exit 0

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。

  • ✅ Sitemap 2026 標準
  • ✅ Breadcrumb Schema
  • ✅ JSON-LD 結構化數據
  • ✅ 內部連結結構

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

002 記分自動守門從未進入 main,但 AGENTS.md/CLAUDE.md 記載為現行 pre-commit 第 6 步

1 participant