ci(scripts): 002 記分守門移植進 main——pre-commit 第 6 步+CI 聚合強制(#881) - #884
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned Files
|
|
✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。
|
There was a problem hiding this comment.
💡 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".
- `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>
|
✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。
|
There was a problem hiding this comment.
💡 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".
- `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>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
- 新增四行條目但 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>
There was a problem hiding this comment.
💡 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".
- 條目行檢查只比對前綴,`- 原因:` 或 `- 解法:` 留空(含僅空白)仍全綠, 使缺 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>
There was a problem hiding this comment.
💡 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".
- 已堵 `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>
There was a problem hiding this comment.
💡 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".
- 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>
There was a problem hiding this comment.
💡 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".
- `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>
2d5a673 to
814da08
Compare
- `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>
- 第十二破口:工作區有檔、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>
|
✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。
|
1 similar comment
|
✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。
|
- 移植 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>
|
✅ SEO 審計通過!所有 2026 標準驗證項目都符合要求。
|
Closes #881
問題
002 記分守門(
scripts/verify-002-log.mjs)從未進入 main:git merge-base --is-ancestor 3321f3e34 origin/main→ NO(pre-commit 守門,feat(scripts): 002 記分 pre-commit 自動守門(issue 608) #652)git merge-base --is-ancestor bcd52c15f origin/main→ NO(CI 端強制,ci(scripts): 002 記分守門 CI 端強制——base-ref 模式驗 PR 最終態 vs merge-base #694)git log --diff-filter=D origin/main -- scripts/verify-002-log.mjs→ 空(從未出現,非 landed 後被刪)git show origin/main:.husky/pre-commit | grep -c verify-002-log→ 0漂移的真實形狀(修正 #881 開卡時的前提)
Issue #881 原本記載「
AGENTS.md與CLAUDE.md都明文記載 pre-commit 有第 6 步」。實際查證:origin/main的這兩份文件從未提過verify-002-log.mjs,它們記載的是 5 步驟 pre-commit,自身完全一致。宣稱 6 步驟的文字只存在於 experiment 分支,以及主 checkout(feat/single-fold-fluid-fit,M 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.mjsbcd52c15f版(含--base-ref)validate002核心,無雙實作.husky/pre-commit第 6 步--base-ref強制test:root,pre-push 覆蓋@types/node+scripts/tsconfig.jsonCI 端為何比 pre-commit 更關鍵(實證)
對 PR #857 真實 head(
493c17fe2)逐 commit 跑 pre-commit 語意:11 個 002-touching commit 全數綠燈,但 CI base-ref 模式對同一 head:
對照組 PR #880 head(
92305bede):002 記分守門通過exit 0。對 main 現存 002 的實跑
- ID:共 519 行,含模板行 1 行)globalErrors: []、重複 ID 0、累計 +251 檔頭解析正常content_type/topics等)→ 不擋(歷史條目格式錯誤不回溯)解析器修正(本 PR 新增,非來源版)
歷史檔有 2 處漏空行使多筆條目黏成一塊(8 行 2 筆、12 行 3 筆),
block.find只取首個 ID → 3 筆條目對 ID 唯一性與「歷史條目不可刪除」防護完全隱形:reward-rw-theme-ssot-drift-convergencereward-starpuff-v17-gamescene-strangler-debt-train(2026-07-20)neutral-starpuff-t2a-changeset-release-intent(2026-07-22)另有 3 個超過 4 行的區塊,實為單一條目帶
content_type/topics/keywords/related_entries額外欄位、非黏合,不造成隱形。初版把這 3 個一併算進「漏空行」才得出 5/6。複驗方式(舊解析器 vs 新解析器實跑 main 真實檔):
修法:解析器補「
- 日期:」為次要邊界(空行仍為主要邊界)。納管條目 515 → 518。歷史格式錯誤仍只對新增條目生效。補 2 案回歸鎖,拔掉邊界後轉紅、還原後綠。故障注入驗證(對 main 現存 518 條目實跑)
檔頭計數(reward 3…)與本次新增條目(reward 1…)不符累計總分斷鏈:前版 251 + 本次 1 = 252,檔頭為 260ID 重複:「reward-build-commit-sha-ssot-zeabur」歷史條目不可刪除,缺失 ID:「penalty-starpuff-t6-w16-shard-respawn-mistake」條目行數應為 4 行(實際 3 行)日期格式應為 YYYY-MM-DD:「2026/07/26」新增條目 ID 必須以 reward-/penalty-/neutral- 開頭新增控制項
AGT-LOG-03:002 條目必須集中在單一 commitpre-commit 以「本 commit 新增條目」對帳、CI 以「PR 聚合淨變化」對帳,兩者在 002 分散多 commit 時必然互斥。臨時 repo 三情境實證:
故新增
AGT-LOG-03並寫入兩份文件。本 PR 自身即依此規則落盤(只有 1 個 002-touching commit)。文件同步前後對照
AGENTS.mdAGT-PC-01pre-commit5 步驟pre-commit6 步驟AGENTS.mdQuality GatesAGENTS.md002 SSOT--base-ref、AGT-LOG-03、rebase 例外AGENTS.mdQuality GatesQuality Checks(002 記分守門,PR 專屬)」(來源分支從未寫過)AGENTS.md控制矩陣AGT-LOG-03CLAUDE.md002 SSOTAGT-LOG-03)+ rebase 例外殘留檢查:
grep "5 步驟" AGENTS.md CLAUDE.md README.md docs/→ 0 命中。驗證
vitest run scripts/__tests__/verify-002-log.test.ts→ 26 passed(24 移植 + 2 自補)pnpm test:root→ 55 passed(5 files)pnpm lint→ 0 error(9 warning,與 main 基線相同)pnpm typecheck→ 全 9 專案通過pnpm format→ All matched files use Prettier code styletypecheck+test+build:ratewise)→All pre-push checks passed!node scripts/verify-002-log.mjs --base-ref origin/main→ 通過附帶影響(如實揭露)
@types/node ^24.13.3後,原本浮動到 26.1.1 的 peer 收斂為 24.13.3,對齊engines ^24.0.0與 8 個 app 的宣告。純型別套件,無執行期影響。scripts/__tests__/lighthouse-production.test.ts:scripts/tsconfig.json加入 node 型別後readFileSync回傳型別由error轉string,prefer-regexp-exec開始生效 →String#match改RegExp#exec(行為等價,來源 commit 同樣修法)。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既有刪除判定語意對齊。補 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 不可靠推導」,不是「做不到」:
Tools/AGENTS.md記載 ratewise 2026H2 的 PR base 一律指向experiment/ratewise-product-2026h2。硬寫origin/main會對這類分支算出錯誤 merge-base → 錯誤 previousTotal → 假紅或假綠。而假綠比沒有守門更危險。@{upstream}指向自己的遠端追蹤分支不是 base;要正確就得引入設定檔或環境變數,等於為了保留舊 SOP 而增加一層可被設錯的狀態。github.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已評估但不採用的替代方案(此為設計取捨,非技術必然)」,明載:origin/main→ 錯誤previousTotal→ 假綠比沒守門更危險)、本機無權威 base 來源、CI 已有零猜測的base.sha--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改為
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]難定位 → 已修257a1fe76parseEntries改先解析 ID,錯誤訊息以 ID 為定位字串(取不到才退回首行)。500+ 條目的檔案裡只引用日期行無法指出是哪一筆。行數/行前綴/日期格式三類訊息皆已帶 ID。審查收斂(round 3:Codex 追加 P2 — 空白 ID 繞過記分)→ 已修
6c3426780真實破口。新增四行條目但 ID 寫成
- ID:(空值)時entry.id留成空字串,newEntries的 truthy 篩選把它排除、唯一性檢查也continue跳過,而模板/日期/前綴檢查都不觸發——只要檔頭不動就全綠,等於憑空插入一筆不計分的條目。修法:
parseEntries以 trim 後的值判定 ID,空白視同缺少 ID,措辭與「缺 ID 行」分開以利定位。補 2 案,紅綠驗證通過(32 passed)。回溯影響為零——
df2033f51/origin/main/HEAD三個參照點皆 0 筆無 ID 條目,不會回溯擋既有 commit。與 Nit A(同 ID 內容被改寫)的區別:那條有合法用途(精確性修正,本 PR 正在用),這條沒有——空白 ID 純粹是漏洞。
審查收斂(round 4:Codex 追加 P2 — 空白原因/解法欄位)→ 已修
f88ade565與 round 3 同族:行檢查只比對
startsWith前綴、不看內容,故- 原因:/- 解法:留空(含僅空白)仍全綠,讓缺 root cause 或 resolution 的紀錄通過兩道守門。修法:兩欄取值後 trim 判空(抽為
CONTENT_PREFIXES)。補 2 案,紅綠驗證通過(34 passed)。回溯影響為零(三個參照點皆 0 筆)。四行模板的內容層檢查現已完整,無第三個同族缺口:
- 日期:由DATE_RE覆蓋(空值不符格式必紅)、- ID:於6c3426780修畢、- 原因:/- 解法:本次修畢。監控席回報:既有條目欄位可被掏空 → 已修
64bc78932(PM 裁決範圍)已堵住
git rm整份刪除後,「保留檔案與 ID、把條目內容清空」是等效的規避路徑,實質同樣湮滅 penalty 證據。採用的規則(PM 裁決,範圍精準收窄):某欄位在基準版為非空時,修改後不得為空(含刪掉整行、留空值、只剩空白)。日期/原因/解法適用;ID 被掏空由既有的「歷史條目不可刪除」攔下。歷史上本來就為空的欄位維持豁免。
不採用「回溯驗證所有既有條目的四行完整性」——main 上有 218 筆歷史條目無標準 ID 前綴、3 筆帶額外欄位,全面回溯會讓守門對現存資料直接報錯,而修正歷史條目又觸犯不可刪改原則,形成死結。
判準:「有沒有從有變成無」,而不是「內容有沒有變」
這條分界是刻意設計,已寫進
AGENTS.md(新增小節「為什麼堵『掏空』但不堵『改寫』(此分界為刻意設計,不是漏做)」)與CLAUDE.md摘要:回歸測試(含真實 diff 正向對照)
補 5 案(日期/原因/解法留空、既有條目整行刪成只剩日期與 ID、正向對照),紅綠驗證:拔掉掏空防護後 3 案轉紅、還原後 39 passed。Codex 描述的具體情境(既有
penalty-*條目縮成只剩日期與 ID,整行刪除而非留空值)另有專屬回歸鎖b70a822e1。正向對照採本 PR 真實發生的精確性修正——即破口規模數字由「5 處/6 筆」更正為「2 處/3 筆」那次 diff 的實際文字,斷言errors為空。另以三組真實 diff 實跑確認零誤判:
實作上
parseEntries抽出 fields map(各欄 trim 後取值),供內容非空檢查與跨版本比對共用,未新增第二套解析。監控席回報:hook 觸發面與 main push 兜底 → 已修
2d5a67322必修 1:hook 觸發條件——一條成立、一條經實測不成立
git rm/git rm --cached--name-only命中 1 → hook 觸發 → 腳本擋下git mv--name-only命中 0 → hook 跳過git diff --cached比對的是 HEAD vs index,刪除會以D呈現故路徑仍列出;git mv則因 rename 偵測只列新路徑(--name-status才看得到R100 舊路徑 新路徑)。端到端實測:所以
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 前:
基準取
github.event.before而非HEAD~1:前者是本次 push 前的 main tip,可涵蓋一次推多個 commit(正是直推情境的樣貌),而HEAD~1只看得到最後一個。merge commit 的before即第一父,語意等同「main 上這次多了什麼」。release commit 不動 002 故自動跳過。分支初建/force push 後before為全零無法解析,該情境跳過而非誤紅。第五條繞過路徑(自主排查找到並修掉):decoy
## 條目區段parseEntries用findIndex只取第一個## 條目。攻擊者在真區段前插入 decoy 區段、抄齊全部歷史 ID 的四行 stub,即可滿足刪除防護;真區段從此永久落在守門視野外,後續 commit 可任意處置。已改為區段必須唯一(「## 條目」區段必須唯一(找到 N 個))。繞過路徑排查清單(10 條內容層 + 7 條 git 層,全部實跑)
內容層(對照基準版含
penalty-old-incident):- ID:半形冒號)##標題截斷區段## 條目區段## 條目標題git 層:
git rm/git rm --cachedgit mvgit update-index --skip-worktree.gitattributes標-diff--name-only仍列出);無條件執行後更完全無關LOG_PATHgit merge產生的 merge commit已知且刻意不補的缺口:merge commit 不走 pre-commit
實測確認
git merge觸發的是pre-merge-commit而非pre-commit,本 repo 未設前者:刻意不補
pre-merge-commit:git merge origin/main併入 main 側的 002 條目時,staged vs HEAD 會把它們全數視為新增而誤紅——那是合法工作流。此情境由 PR CI 與新增的 main push CI 覆蓋,處置與既有的 rebase 情境一致(AGENTS.md 已載明 rebase 需手動跑守門)。回歸測試(這輪鎖的是觸發面,不是腳本層)
pre-commit 第 6 步無條件執行守門,不得以 git diff 判斷觸發ci.yml 對 pull_request 與 main push 都掛守門,且置於 install 之前pre-commit:git mv 改名 002 必紅多個「## 條目」區段時回報全域錯誤(decoy 區段盲區)三項各自紅綠驗證:還原 hook 的 diff 條件 → 該案轉紅;移除 ci.yml push 步驟 → 該案轉紅;移除區段唯一性 → 排查腳本回報「未擋 1 條」。43 passed /
test:root72 passed。真實 002 三個參照點(df2033f51/origin/main/HEAD)globalErrors 皆為空、真實 diff 零誤判。審查收斂(round 5:Codex 三條,含一條 P1)→ 已修
d93cbef7eP1 main push 兜底對它自己要防的情境無效
runAgainstBaseRef一律取merge-base(ref, HEAD)。force push 時before不是 HEAD 的祖先,merge-base 會退回更早的共同祖先,使before與祖先之間新增的 penalty 條目在改寫後消失也驗不出來。臨時 repo 重現:修法:新增
--base-commit直取該 commit 作基準,ci.ymlpush 步驟改用之;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 完整重現:一處與審查席環境不同:
LANGUAGE=zh_CN單獨在我機器上沒有觸發(仍輸出英文),只有LC_ALL會。修法同時涵蓋兩者,故不影響結論,但如實記錄差異。修法
四處 git 呼叫(
cat-file/show/merge-base/rev-parse)統一傳env: { ...process.env, LC_ALL: 'C', LANGUAGE: 'C' }。註解記錄了審查席標為低信心殘留的那項:鎖 C locale 同時鎖住訊息穩定性,等於把「跨 git 版本翻譯字串變動」這個無法實測的風險一併降低。
回歸測試(三案)
非英文環境怎麼驗證的:本機
locale -a有zh_CN.UTF-8,且 Homebrew git 2.55 內建 gettext 翻譯(share/locale下有語系目錄),所以行為對照在本機是真測試。但 CI(ubuntu runner)不保證裝有中文 locale——沒有翻譯時 git 本來就輸出英文,那兩案會變成不會誤紅也不提供額外保證的 no-op。這正是我另外加結構鎖的原因:它不依賴 locale 可用性,在任何環境都能擋住回歸。紅綠驗證:拿掉任一處env: GIT_ENV→ 結構鎖轉紅。七情境 × 雙 locale 實跑,行為完全一致:
002 重算
第十三破口:orphan HEAD → 已修
00f47209c方法層置換只覆蓋了 path 存在性,「無基準版」的語意仍綁在「HEAD 不是 commit ⇒ null」。重現(標準 Git 指令、無 PATH 劫持):
修法未加任何訊息 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(
typescriptcompiler,與 #886 同一套思路):child_process只能具名且未改名匯入;禁 namespace/default,因為cp['execFileSync'](…)無法靠名稱追蹤git()wrapper 的節點範圍內env: GIT_ENV紅綠驗證兩種舊鎖漏接的繞法:
assertRepoUsable敘述修正原敘述隱含「確認過就安全」;改為明說:只在首次使用時確認一次,之後
ls-*若因 repo 中途不可用而非零離開仍會 fail-closed,故不需重複確認。被 orphan 證偽的「rev-parse 失敗只可能是 ref 不存在」推論已從程式碼與AGENTS.md一併移除。驗證
vitest 78 passed(orphan 掏空必紅、真空 repo 仍放行、AST 鎖兩種繞法、措辭鎖改寫版各自紅綠)/
test:root107 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 -els-files/ls-treegit rm --cached六個「不存在」情境有五個變成 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 在結構上不可能,而非事後偵測。另補一道結構鎖禁止退回訊息比對。回歸測試(兩個新情境)
紅綠驗證:還原成
cat-file訊息比對實作後 4 案轉紅(兩個新情境 + 兩道結構鎖),還原後 76 passed。驗證
vitest 76 passed/
test:root105 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、淨 +21(a+b+c=30等於新增條目數、265+21=286),累計 286 正確、僅 delta 行寫成 +1。第一步
8ee2ebc8a(純修復,不新增條目)補回日期後該條目
parseEntries格式錯誤由 4 條歸零,條目總數維持 574(原本即以 ID 行計為一筆)。delta 行寫
+0而非裁決的+21——這裡我偏離了指示,理由如下:檔頭語意是「本 commit 新增條目的淨變化」,不是歷史聚合。寫+21實測必紅:唯一能寫入
+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)並重算修好之後不再需要依賴「歷史條目不回溯」這個特性——該條目現在格式完整。
分支上有 2 個 002-touching commit(修復 1 + 條目 1),符合
AGT-LOG-03:該規則約束的是「新增條目」集中在單一 commit,修復 commit 新增 0 筆。第十破口收斂 → 已修
b88eaeb68Blocking:
gitShow把環境失敗當「物件不存在」git 用同一個 status 128 同時表示「路徑不存在」與「repo 不可用」,而舊碼只排除
ENOENT。三種環境失敗實測全部 fail-open:修法與複審建議略有不同:建議的 regex 只含
does not exist,會誤擋「尚無任何 commit」的初始 commit 路徑(該情境 git 回invalid object name 'HEAD'.)。我先窮舉實測守門實際會遇到的全部四種合法「不存在」訊息,改用逐條列出的顯式清單:path '…' does not exist in '…'path '…' does not exist (neither on disk nor in the index)git rm --cached)path '…' exists on disk, but not in the indexinvalid object name 'HEAD'.第三種是我最初的 regex 也漏掉的——
git rm --cached的測試立刻轉紅才抓到,正好證明窮舉比推測可靠。catch區塊逐一稽核(四處)gitShowmerge-baserev-parseisDirectRunisDirectRun原本吞掉例外並退回字面比較,比不中就靜默視為「非直跑」——main()不執行卻 exit 0,正是假成功。該情境執行期無法穩定構造(node 必須先讀到檔案才能執行),故以結構鎖把關而非行為測試,這點如實標註。必修 2:文件矛盾——擴充關鍵字後多抓到一處
AGENTS.md:217/CLAUDE.md:109的「不依賴解析是否成功」與實作相反。加入被取代的舊措辭後,另掃出腳本:204同一問題(複審未點名),共修 4 處。必修 3:把判斷題變成清單題——並且機械強制
不只寫進文件,直接做成測試:固定六個檔案的掃描範圍 ×
SUPERSEDED_PHRASES已取代措辭表,任一命中即紅。下一任不需要判斷「什麼算涉及守門行為」,唯一要做的是改寫行為時把舊說法加進清單。(測試檔自身在掃描範圍內,故剔除清單宣告區段避免自我命中。)
應修 4
headHeaderLine的headContent ?已對齊hasBase。回歸測試
新增 5 案(壞 git stub、
GIT_DIR無效、.git權限、isDirectRun結構鎖、措辭漂移檢查),各自紅綠驗證。vitest 70 passed/test:root99 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.github/workflows/ci.ymlAGENTS.mdCLAUDE.mdscripts/__tests__/verify-002-log.test.tsdocs/SEO_MASTER_SSOT.md:15AGT-LOG-01編號,未複述控制內容)package.json:36共修 8 處。最終掃描
約 50ms|毫秒級|基準版無法解析時跳過|p50 約 120全 repo 零命中。必修 4:
gitShow契約閉合——你問的那題答案是「巧合」複審席正確。我另找到一個可直接觀測的形式:把 git 移出 PATH 後整道守門靜默通過。
修法:先
git cat-file -e判存在性,存在卻讀不出即向上拋,直跑入口統一捕捉為 fail-closed。重跑複審席那個攻擊(重寫檔頭對齊 compliant 條目、寫入 +999999):
契約閉合後
headContent=null只可能發生在物件確實不存在(git cat-file -e HEAD:002已驗證存在),故該攻擊在真實 repo 無法觸發。必修 5:區段外獨立
- ID:行造成刪除假陽性改為基準版可解析時只採信解析結果;不可解析時才退回全檔原始文字掃描(CASCADE 防護不受影響,且該情境本就 fail-closed)。
必修 6:ID 唯一性休眠缺陷
應修 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:root89 passed,10 條繞過路徑排查維持全數擋下。終審收斂(Sonnet 79/84、Grok 64/73)→ 已修
6ef05cca2Blocking — CASCADE:基準版解析失敗時刪除防護被完全掏空
parseEntries(headContent)只取.entries丟棄globalErrors,基準版不可解析時headEntryIds為空集合,刪除檢查落入真空。重現(檔頭算術刻意保持正確,讓刪除檢查成為唯一防線):雙保險修法:基準版有
globalErrors即 fail-closed;刪除比對另取「解析結果 ∪ 原始文字掃描- ID:」不依賴解析成功。修後兩道各自獨立命中。修法本身造成的回歸(自行抓到並修):raw 掃描讓被隱藏的條目仍出現在 staged 集合,使「插入
##標題截斷」「條目搬到區段之前」兩條既有防線失效——繞過路徑排查腳本當場報「未擋 2 條」。補hiddenIds檢查(基準版解析得到、待驗版只剩原始文字者一律擋),10 條排查回到全數擋下。同族第八條(自主稽核發現):基準版讀不出累計總分時靜默跳過總分鏈
改為僅「無基準版」(初始 commit)才跳過,讀不出即 fail-closed。
HIGH — 雙 flag 互斥
原迴圈先匹配
--base-ref使誤用得到假綠。改為同時指定即失敗。錯誤路徑 fail-closed 全面掃描(13 條實跑)
--base-ref/--base-commitref 無法解析、缺值、雙 flaggit rm --cached、git mvgitShow('HEAD:…')非預期回 null(誤判為初始 commit)before全零契約已以表格寫入
AGENTS.md,供新增分支時遵循。文件逐句對照掃描:共修 12 處
列舉兩份文件中提及此守門的每一句再逐句對實作,而非單點修補:
AGENTS.mdAGT-LOG-01git commit前更新」→「每個 PR 更新,落盤時機依AGT-LOG-03」(與AGT-LOG-03字面衝突)AGENTS.md控制矩陣AGT-LOG-04殘餘風險列AGENTS.mdPhase 4 步驟 1AGENTS.md記分段AGT-LOG-03AGENTS.md第 6 步AGENTS.md區段唯一段AGENTS.mdhook 開銷AGENTS.mdCI 章節CLAUDE.md:107AGT-LOG-03CLAUDE.mdExecution SOP 第 6 步CLAUDE.md:109CLAUDE.md:113--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/變更;守門與其測試對starpuff/assetPlan/SHARED_LEVEL_KEYS/assets.ts的引用計數為 0,僅讀 002、.husky/pre-commit、.github/workflows/ci.yml。pnpm --filter @app/starpuff test979 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,故獨立驗算)檔頭:
> 本次分數變化:+5(reward 8、penalty 3、neutral 0)|累計總分:+270條目整理:七項修正依性質分組為 4 筆 reward(非逐項流水帳)
reward-002-log-gate-content-emptiness-bypassesreward-002-log-gate-trigger-surface-bypassesgit mvhook 跳過、main push 無 CI gate、decoy 區段盲區——同屬「不改內容、改達成形式」,附 17 條排查reward-002-log-gate-direct-run-and-locatorreward-002-log-gate-changeset-scope-boundaryAGT-VER-01適用界線文件化reward-002-log-gate-push-base-and-header-sign另記 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)兩個新步驟行為與設計完全一致:
GitHub 接受兩個步驟定義(含全零 SHA 判斷的
if表達式),workflow 語法有效。11 checks 全 pass,mergeable=MERGEABLE state=CLEAN。全閘門(rebase 後重跑)
vitest43 passed|test:root72 passed|@app/starpuff979 passed|pnpm lint0 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 項跟進)→
13695c9ce/fc0914a28/9666bfcaeGrok 收官確認 90/92 APPROVE 後,依「全席 ≥95」硬規則把 5 條跟進項全數收掉:
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 expire+gc --prune=now)的極端情境本機與真空 repo 結構上無法區分——已登記AGENTS.md殘餘風險表,由 CI 以 GitHub 事件基準 SHA 在乾淨 clone 上兜底。auditGitWrapperStructure單一函數。堵變數別名(綁定識別字只能作 import 綁定或直接呼叫 callee)、eval/new Function/getBuiltinModule/動態 import/字串夾帶 API 名;env: GIT_ENV與 GIT_ENV 定義本身升級 AST PropertyAssignment 驗證(舊字串 includes 會被註解裡的字面假陽性通過)。12 條 mutation 元測試釘住每條繞法必紅。SUPERSEDED_PHRASES舊常數名修正(實碼已改SUPERSEDED_PATTERNS),並把舊常數名與舊「無基準」判準敘述加進措辭鎖自我偵測。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 逐條裁決執行)
node:child_process/promises靜態 import(Grok 標 HIGH)→ PM 技術否決,降級為防禦性加固:PM 親測該子路徑不是 Node 內建模組——寫進 wrapper 檔的實際後果是ERR_UNKNOWN_BUILTIN_MODULE載入失敗 → pre-commit 第 6 步大聲 fail-closed,不是靜默繞過;且既有「匯入必須恰為execFileSync」斷言已擋住spawnSync/exec等真實存在的同族 API(故 Grok 建議的擴充禁用清單不採納)。仍以 1 行成本把 specifier 匹配放寬為「路徑段含child_process者皆檢」(/(^|[:/])child_process(\/|$)/)並補 mutation,徹底消滅此爭點——程式註解與 002 條目均誠實標明屬防禦性加固、非修補已知繞過。require/createRequire有禁令無 mutation(MEDIUM,採納):Grok 實測把兩字從禁用清單移除後既有 mutation 仍全綠——真實覆蓋缺口。補 2 條注入 mutation。LANGUAGE: 'C'有 AST 檢查無 mutation(LOW,採納):對稱補 1 條弱化 mutation。vm/worker_threads/process.binding(Open Question)→ 維持不防、書面出界:能力邊界註解明列不防類別(字串拼接/template literal/Reflect.get/node:vm/worker_threads/process.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)|累計總分:+31934 筆條目集中於單一聚合 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 語意同過pnpm test:root126 passed、lint 0 error/9 既有 warninggh pr checks 884:Quality Checks pass(7m52s)、Lighthouse CI pass、E2E Smoke pass、CodeQL/Analyze×3 pass、SEO Audit pass、Dependency Review pass,零 fail(E2E Full 等為條件跳過)mergeStateStatus: CLEAN