08 品質校驗時間軸

從改第一行字到回歸驗收,每一道維持品質的校驗都列在這裡:它什麼時候跑、擋什麼、不能證明什麼、自己怎麼手動跑、失敗時看哪裡。本章是導讀;正式規則與操作細節以 docs/agents/ci-checks.md、development-workflow.md §3、issue-run.md 與各腳本檔頭為準。

品質校驗時間軸(互動圖:可切換主題、搜尋節點;全頁開啟)

互動版:diagrams/08-quality-gates.html(來源 diagrams/src/08-quality-gates.workflow.json)

強度分級

強度意思例子
本地強制git hook 或 Claude Code hook 擋下,agent 繞不過(claude-hook-git-bypass.mjs 連 --no-verify 都擋)pre-push hook、reference-transaction 守衛、.claude/settings.json deny
流程內skill 或腳本在固定步驟呼叫,失敗即停;但沒有 hook 強制,另開終端手動 commit 可以跳過check-contracts.sh 內的各 check-*、focused 測試、Codex review
人工觸發只有人按下才跑GitLab 手動 web pipeline

三種強度都只在本機或 GitLab Runner 上跑;沒有任何一道是 GitLab 伺服器端強制(「pipeline 綠才能 merge」未啟用)。

1. 編輯時(agent 工具呼叫)

校驗強度觸發點檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
.claude/settings.json deny 清單本地強制Claude Code 每次工具呼叫拒絕 git push、glab mr、glab issue create、psql/mysql/sqlcmd/sqlplus、編輯 .claude/settings*.json人在自己的終端做同樣的事不受限;也不證明 agent 透過其他指令(例如腳本包裝)沒碰到目標無;規則只能由人改 .claude/settings.json工具呼叫被拒時 Claude Code 直接顯示規則;格式錯的規則由 check-claude-settings.mjs 在 push 前抓出
PreToolUse hook claude-hook-git-bypass.mjs本地強制每個 Bash 工具呼叫(settings.json 的 hooks.PreToolUse)指令含 -c core.hooksPath=、git config core.hooksPath …、--no-verify、git commit -n 即 exit 2 拒絕只比對字串,寫進腳本檔再執行的繞過抓不到;也不檢查指令的其他內容printf '{"tool_input":{"command":"git commit -n"}}' | node scripts/claude-hook-git-bypass.mjs(exit 2 代表命中)stderr 一行「拒絕:指令以 … 繞過 git hook」;規則在 scripts/claude-hook-git-bypass.mjs 的 BYPASS 表
concurrent-edits.mjs流程內開工前,對要改的檔案跑一次其他 worktree 對同一檔的未合入修改、整合分支自 baseline 以來對這些檔的 commit只看檔案層級重疊,不看語意;跑完之後別人再改就不知道node scripts/concurrent-edits.mjs <file>…stdout 列出 worktree 與 commit;CLEAR 代表沒有重疊

2. commit 時

校驗強度觸發點檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
check-commit-messages.mjs流程內(commit 時)/本地強制(push 時)/issue-run 每次 commit 後;check-before-push.sh 第一步再對 HEAD 跑一次非 merge commit 的訊息符合 git-standards:<類型>: <描述>,類型限 feat/fix/docs/style/refactor/perf/test/chore只讀 git objects,不看 diff 是否與訊息相符、不看範圍node scripts/check-commit-messages.mjs HEAD <baseline>(檢查 <baseline>..HEAD;只給 HEAD 時改檢查尚未在任何 remote 的 commits)stderr 列出不合格的 SHA 與原因;改訊息用 git commit --amend 或 rebase
git diff --check流程內文件任務 commit 前行尾空白、衝突標記不檢查內容git diff --check(已 commit 的用 git diff --check <baseline>..HEAD)git 直接印 path:line: trailing whitespace

沒有 pre-commit hook:commit 本身不被擋,所有錯誤留到 push 前才會被 hook 強制抓出。

3. 實作中(只跑本單的驗證)

校驗強度觸發點檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
compile流程內每次改 Java受影響模組與其 consumer 編譯通過行為正確;producer 編譯通過不代表 consumer 相容bash gradlew :<module>:compileJavaGradle 輸出
spay4SourceCheck流程內改 spay4-bo Java 後、跑 Docker 測試前ConceptPackageStructureTest、SupplyMethodDocumentationContractTest 兩個來源契約(不起 Spring/DB)其他欄位文件、方法數上限、任何行為bash gradlew :spay4-bo:spay4SourceCheck --rerun --no-build-cache --offline --max-workers=2 --console=plainGradle 輸出;定義在 spay4-bo/build.gradle
spay4FastCheck流程內spay4SourceCheck 之後、Docker 測試之前;isolated runner 每輪也先跑spay4-bo 全部 DB-free 測試(拿掉 native/G6 開關,Testcontainers 測試自動 skip)schema、交易、鎖、真實 DB 語意;skipped 不算通過bash gradlew :spay4-bo:spay4FastCheck --rerun --offline --console=plainGradle 輸出;runner 內失敗為 FAST_CHECK_FAILED
focused 測試(run-docker-tests.sh)流程內行為變更時,只跑本單相關的測試類別指定測試在本機 Docker MySQL 上通過;腳本先 docker-ensure.sh,執行中每分鐘探測 Docker其他模組、未指定的類別、slow 層;Docker 凍結時結果作廢SPAY4_TESTCONTAINERS_ENABLED=true bash scripts/run-docker-tests.sh :spay4-bo:test --tests '<class>'(腳本不代設開關,沒設則 Testcontainers 測試會 skip),結果用 node scripts/junit-summary.mjs spay4-bo test <class> 讀stdout 最後一行 DOCKER_UNAVAILABLE/DOCKER_LOST 代表環境問題,先 bash scripts/docker-ensure.sh 再重跑;測試失敗看 junit-summary.mjs 的 per-testcase 結果

模組對應表與 Legacy 模組規則見 development-workflow §3。文件與工具任務不跑這一節。

4. 候選前(每張 Issue 固定跑)

校驗強度觸發點檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
check-test-guardrail.sh流程內(也在 check-contracts.sh 內)每張 Issue 候選前每個 scripts/*.test.mjs、scripts/test_*.py、docs/spay4/tools/*.test.mjs 都列在 check-contracts.sh 或 check-before-push.sh測試內容是否合理、是否真的會跑到(只做字串比對)bash scripts/check-test-guardrail.shstderr「未接入 guardrail 的測試:<path>」;把檔名加進 check-contracts.sh 的 node --test 清單
check-sql-impact.mjs流程內每張 Issue 候選前baseline 到目前 worktree 是否觸及 schema 相關檔;Issue 宣告 SQL prerequisite:無 而命中即 exit 1只判斷「有沒有碰到」,不判斷語意;SQL impact ack: 列出的路徑視為已裁定node scripts/check-sql-impact.mjs <baseline> --issue <iid>stdout 列出命中路徑;exit 1 是停點,走 SQL 路徑 C
pinned-sources.mjs流程內跑任何 Docker 測試前變更檔是否被其他 tracked 檔以 SHA-256 釘住(compat fixture、site-progress 等),改一行註解也會讓釘住它的測試紅多檔串接後才雜湊的 pin 偵測不到node scripts/pinned-sources.mjs <baseline>每個命中一行 PINNED <path> <sha256> <釘住它的檔案>;更新 pin 或停下來問 Operator
deliver-sync-baseline.mjs流程內step 0、review 前、候選前整合分支領先 baseline 時是否與本單改動或規則檔重疊:不重疊只記錄;重疊且 merge-tree 乾淨就合進來並前移 baseline;有衝突 exit 3合進來的上游變更對本單的影響——merged 後要重讀 ruleFilesnode scripts/deliver-sync-baseline.mjs <iid>(在卡片 worktree、變更已 commit)stdout JSON 的 status/overlap/conflicts;exit 3 時 git merge --abort 或手動解
Codex review(run-codex-review.mjs)/code-review skill流程內候選前;最多兩輪修正Standards(repo 規範)與 Spec(Issue 要求)雙軸 review;Codex 不可用時改用 code-review skill 並標記測試覆蓋與行為正確;review 只看 diff 與 specnode scripts/run-codex-review.mjs <baseline> <spec.md> <worktree> <out-dir> [rulings.md]<out-dir>/review.md;exit 1 時 stderr 一行 JSON,REVIEW_WROTE_REPO 代表 reviewer 動到 worktree
deliver-candidate.mjs流程內review 通過後baseline 必須是 HEAD 與整合分支的 merge-base,才把本單 commits 壓成一個候選 commit候選內容本身的品質node scripts/deliver-candidate.mjs <iid>stdout JSON;exit 1 多半是 baseline 過期,先跑 deliver-sync-baseline.mjs

執行順序與停點見 issue-run Checks。

5. 合入整合分支

校驗強度觸發點檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
merge-into-integration.sh lock 與預約流程內(唯一入口)「結案 n」、批次 Integrate、retro 合入同一時間只有一組「回歸→合入」;候選未含整合分支 tip 時做漂移檢查(DRIFT exit 6);retro 分支與進行中 worktree 重疊時 INFLIGHT exit 7(inflight-overlap.mjs)合入內容的正確性;--accept-drift 只在 Operator 明示不重跑時用bash scripts/merge-into-integration.sh <branch> <message> [--regression <result.json>];狀態 --statusstdout 第一個字:MERGED/CONFLICT/INTEGRATION_BUSY(exit 75,稍後再試,不寫等待迴圈)/DRIFT/INFLIGHT;用法在腳本檔頭
reference-transaction 守衛本地強制任何對 refs/heads/<整合分支> 的寫入(prepared 階段)只放行無變化、單一 parent 直接 commit、fast-forward 到 origin/<整合分支>;merge commit、多 commit fast-forward、改寫歷史、刪除只允許 lock ownerpack-refs 與不帶舊 SHA 的 update-ref -d 是已知縫隙;主 checkout 在 develop/master 或 detached 時不檢查安裝:bash scripts/install-integration-guard-hook.sh;守衛本身由 git 觸發git 拒絕 ref 更新時印出 hook 的 stderr;規則在 scripts/git-hooks/reference-transaction 檔頭
check-before-push.sh流程內(此時)/本地強制(push 時)close-issue.mjs 合入後對 merged HEAD 跑一次同下節;通過且工作區乾淨時寫 pass 標記並印出鎖定 SHA 的 推送指令同下節bash scripts/check-before-push.sh同下節

只改 scripts/、.claude/、docs/、根目錄 *.md 的分支免回歸、可插隊合入,仍要過 check-contracts.sh。

6. push 時

校驗強度觸發點檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
pre-push hook本地強制每次 git push(install-pre-push-hook.sh 安裝一次,linked worktree 共用)推送的每個 commit 必須等於 HEAD、工作區乾淨(含未追蹤檔);對同一 SHA 已有 pass 標記就沿用,否則呼叫 check-before-push.sh;驗收期間 HEAD 或工作區改變即拒絕不是伺服器端強制:--no-verify 或其他 client 可繞過(agent 側由 claude-hook-git-bypass.mjs 擋);不等於 GitLab CI 綠無法手動觸發 hook 本身;跑下列 gate 等價hook stderr 以「拒絕 push:」開頭並說明原因
check-before-push.sh本地強制(hook 呼叫)/流程內(手動)pre-push hook,或手動依序:Node v24.21.0、git/python3 版本、check-commit-messages.mjs HEAD、整套 check-contracts.sh、V4 verifier 語法與測試(verify-v4-schema.mjs、v4-schema-verifier.mjs、push-gate-output.sh 的測試)Java 編譯/測試、真實 DB、GitLab Runner、通知 runtime;手動跑時 HEAD 已在 remote-tracking ref 內會直接略過bash scripts/check-before-push.sh(SPAY_PUSH_GATE_FORCE=1 強制驗收、SPAY_PUSH_GATE_VERBOSE=1 即時輸出)終端只印失敗步驟最後 80 行,完整 log 在 $(git rev-parse --git-path spay-push-gate.log);最後一行 stderr push 前離線驗收失敗:<step>;完整 log:<path>

check-contracts.sh 內的每道 check-*

check-contracts.sh 是本地 push gate 與 GitLab spay4-contract-check 共用的入口;以下每一列都在它裡面依序執行,任一非零即停。這張表由 check-quality-gates-doc.mjs 守著:check-contracts.sh 或 check-before-push.sh 新呼叫一支 check-* 腳本而這裡沒寫,push gate 就會失敗。

校驗檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
check-text-encoding.mjs所有追蹤與未忽略的新文字檔(含兩支無副檔名 hook)是合法 UTF-8、無 BOM、無 NUL;範圍內 symlink 直接失敗內容正確;副檔名清單以腳本為準node scripts/check-text-encoding.mjs只印相對路徑與錯誤類型,不印內容
check-secrets.mjsPEM 私鑰、service account JSON、-firebase.json 等樣式其他形式的密碼或 token(只比對固定樣式)node scripts/check-secrets.mjs只印 path 與規則代碼,不印內容
check-markdown-links.mjsMarkdown 本地相對連結與 #anchor(含 frontmatter 內的 .md 路徑)存在;docs/archive/ 來源不檢查外部 URL(不連線)、連結指到的內容是否仍正確node scripts/check-markdown-links.mjs列出 path:line 與壞掉的連結
check-skill-lines.mjsskill Markdown 單行長度上限(一句一行,減少並行 session 的整行衝突)內容node scripts/check-skill-lines.mjs列出 path:line 與長度
check-legacy-skill-refs.mjsAGENTS.md、docs/、skills 內不再以斜線指令形式推薦淘汰中的 deliver-spec、deliver-batch skill(一律改 /issue-run)其他過期說明node scripts/check-legacy-skill-refs.mjs列出 path:line
check-script-refs.mjs文件提到的 <name>.mjs/.sh/.py/.ps1 在 repo 內必須有同名追蹤檔(腳本改名後文件不會悄悄過期)引用的用法與參數是否正確node scripts/check-script-refs.mjs列出 path:line name;提舊名請去掉副檔名,不加例外
check-claude-settings.mjs.claude/settings.json 權限規則格式(Claude Code 對格式錯的規則只略過並警告,不會失敗)規則的意圖是否正確node scripts/check-claude-settings.mjs列出有問題的規則
check-adr.mjsdocs/adr/ 結構:概念檔 kebab-case、每節前有同編號錨點、編號全域唯一、無完全 Superseded 章節、README 索引一致決策內容node scripts/check-adr.mjs列出 path 與原因
check-quality-gates-doc.mjscheck-contracts.sh 與 check-before-push.sh 呼叫到的每個 scripts/check-*.mjs/.sh(含 run_step 內與多行續行;*.test.mjs 由 check-test-guardrail.sh 管)都出現在本章本章描述是否仍正確,只保證名字沒漏node scripts/check-quality-gates-doc.mjs列出缺漏檔名與修正指示(在本章加一列)
build-workflow-site.mjs --checkdocs/workflow/site/(統一入口站)與 Markdown 重產結果零 diff:忘了重產或手改 HTML 都擋下(README 統一入口站)站的內容與排版是否正確node scripts/build-workflow-site.mjs --check列出不同步的檔名與重產指令
Bash/Node/Python 語法bash -n 各 shell gate 與 hook;node --check 所有 scripts/*.mjs、scripts/lib/*.mjs;py_compile 各 Python 工具與測試執行期行為見 check-contracts.sh 對應行直譯器錯誤訊息
docs/Postman validatorsvalidate-docs.mjs、validate-postman.mjs、update-bo-ui-foundation-postman.mjs --check(A1/A2 collection)全站 Postman inventory 同步、API 實際行為node docs/spay4/tools/validate-docs.mjs 等validator 自己的輸出
node --test 回歸清單與 Python unittestscripts/*.test.mjs 與 docs/spay4/tools/*.test.mjs 全部、run-python-tests.py 分三片跑的 Python 測試;fixture 都在 OS 暫存目錄任何 Java、DB、GitLab、Orca 的真實行為node --test --test-force-exit --test-timeout=120000 <files>node:test 的 TAP 輸出;Python 只印失敗分片的 log
check-test-guardrail.sh同第 4 節同第 4 節bash scripts/check-test-guardrail.sh同第 4 節

push 前另有唯讀的範圍報告 report-push-scope.mjs(交付範圍報告),列出要推出去的 commits 與路徑,不是驗收。

7. GitLab 手動 CI

校驗強度觸發點檢查什麼不能證明什麼本地等價指令失敗看哪裡
spay4-contract-check人工觸發GitLab「Run pipeline」(web)指定分支bash scripts/check-contracts.sh,在 node:24.21.0-bookworm-slim 上,15 分鐘 timeout與本地相同;加上 Runner 可用才算接上 CIbash scripts/check-contracts.shGitLab job log
spay4-schema-verifier-test人工觸發同上,與上一個 job 平行V4 verifier 語法與測試(before_script: [],不裝任何套件)真實 target schema、READY 狀態(合成 READY 只在測試裡)node --check scripts/verify-v4-schema.mjs && node --check scripts/lib/v4-schema-verifier.mjs && node --test --test-force-exit --test-timeout=120000 scripts/verify-v4-schema.test.mjsGitLab job log

現況:自動 pipeline 暫停,push/MR/tag/schedule 都不會起 pipeline,只接受手動 web pipeline;兩個 job 都沒有 allow_failure、retry 或 cache。啟用條件、安裝細節與驗收邊界見 docs/agents/ci-checks.md。

8. 回歸分級

校驗強度觸發點檢查什麼不能證明什麼手動怎麼跑失敗看哪裡
daily 層 :spay4-bo:test流程內日常、focused 之外想跑整個模組時排除 @Tag("slow") 的全部測試;runner 帶 --base <整合分支> 時重 class 只在 diff 命中其 guard 時跑slow 層、被選跑略過的重 class;不是驗收SPAY4_TESTCONTAINERS_ENABLED=true python3 scripts/run-spay4-isolated-tests.py --ref HEAD --base <integration-branch>(runner 不自動啟用任何環境 gate;見 runner README)runner 的 result.json;CLOCK_UNSTABLE 先 --clock-probe 量時鐘
acceptance 層(固定 SHA)流程內候選 commit 要合入前;批次只跑一次統一回歸SPAY4_SLOW_TESTS_ENABLED=true、SPAY4_TESTCONTAINERS_ENABLED=true,不給 --base,全部 class 都跑;通過才能帶 --regression 合入排除任何重 class 或 slow 的 run 不算驗收;--no-reservation 的量測 run 永遠不是合入證據node scripts/queue-regression.mjs --artifacts-root <dir> [--merge <message>] --detach,再 --wait <dir>queue-regression.json 的 status(REGRESSION_FAILED/SYNC_CONFLICT…);result.json 的 fullAcceptance 必須為 true
queue-regression.mjs 預約流程內acceptance 層起跑前以 merge-into-integration.sh --reserve --wait 嚴格 FIFO 排隊,候選落後整合分支 tip 時先同步再跑;失敗自動釋放預約排隊中別人的改動——所以才要同步後再跑同上stdout 最後一行 JSON;merge-into-integration.sh --status 看誰持有預約

分級定義與重 class 清單見 SPay4 測試方式;排隊規則見 issue-run Candidate and acceptance。

整體邊界