約束驅動開發
總覽
此套件中的其他技能描述了「好的程式碼」長什麼樣子。code-review-and-quality 提供了五個審查維度,test-driven-development 定義了開發循環,security-and-hardening 列出了威脅清單。這些都存在於代理程式閱讀的文字中,但可能被忽略,而且隨著對話結束而失效。
本技能產出不同的東西:一份關於此專案品質門檻的書面記錄,包含具體數字,能存續於對話之外,並可自動檢查。
原因很重要。當你撰寫程式碼時,閱讀程式碼就能判斷其好壞。但一個代理程式一個下午就能寫出你當週都看不完的程式碼,所以判斷的責任必須從你的腦袋轉移到能夠在開發循環中運行的檢查機制上。這些檢查必須存在,需要你實際選擇的數字,並且必須在接近工作產出的時間點觸發,以便代理程式自動修正其輸出。
規格驅動開發定義了要建構什麼,測試驅動開發證明了它能運作,而約束驅動開發則定義了「夠好、可以交付」的標準——在任何人於 pull request 中爭論之前就定義好。
使用時機
請在以下情況套用此技能:
- 啟動一個新專案或重要功能,且尚未寫下任何品質門檻時
- 使用者要求「設定約束條件」、「加入品質關卡」、「定義我們的標準」,或「阻止代理程式產出垃圾」時
- 代理程式正在大量產出程式碼,而沒有人會逐行閱讀時
- CI 有檢查機制,但沒有人能說哪些檢查會阻止合併、哪些只是裝飾時
- 每次開 PR 都要爭論測試覆蓋率、效能或無障礙性數字,而不是一次決定好時
- 你正要執行
/build auto或任何自動化循環,而唯一能阻止它合併到主線的,只是一套同樣由代理程式撰寫的測試套件時
何時不該使用:
- 專案已經有
CONSTRAINTS.md,且使用者沒有要更改它——此時應直接閱讀並遵循該文件 - 一次性腳本、技術驗證、拋棄式原型
- 使用者目前需要 code review(請用
code-review-and-quality)或建立 CI 管道(請用ci-cd-and-automation) - 預計壽命只有兩週、還在產品-市場契合前的程式碼——底層的最低標準仍然值得建立,但其餘部分則否
載入約束條件
訪談需要與真人互動。請勿在非互動式環境中執行此訪談(例如 CI、/loop、自動化執行)。如果在上述情境中缺少約束條件,請套用下方的「最低標準」,註明你已這麼做,並將其餘部分標記為待人類處理。
流程
步驟 1:先偵測,再提問
永遠不要詢問你已經可以讀取到的資訊。在第一個問題之前,先收集:
| 項目 | 查看位置 |
|---|---|
| 語言與技術棧 | package.json, pyproject.toml, go.mod, Cargo.toml |
| 測試框架 | dev dependencies, test script, 現有測試檔案 |
| 現有 linter | eslint.config.*, biome.json, .ruff.toml |
| 當前測試覆蓋率 | coverage/ 目錄,或先執行一次測試套件 |
| CI 設定 | .github/workflows/, .gitlab-ci.yml |
| 代理程式框架 | .claude/, .codex/, AGENTS.md |
用兩行文字報告你的發現,然後只問還不清楚的部分。
步驟 2:四個問題,每個都有預設答案
遵循 interview-me 中「一次只問一個問題」的原則,但有一項改變:這裡每個問題都有預設答案,所以「我不知道」也是一個完整回答,並且仍然能產出一份可用的設定。
Q1: 除了最低標準之外,以下哪些你想要強制執行?
(a) 新程式碼的測試覆蓋率
(b) 安全性掃描
(c) 效能預算
(d) 無障礙性
(e) 架構邊界
猜測:(a) 和 (b)——你已經有測試框架,而且正在處理使用者輸入。
不確定時的預設值:(a) 和 (b)。
說明每個選項的成本:(c) 和 (d) 需要一個可執行的 URL,(e) 需要撰寫規則檔案。
Q2: 當代理程式在執行任務途中,某項檢查失敗時,應該要阻止還是僅警告?
猜測:阻止。因為你在無人監看的情況下執行代理程式,而沒有人會去閱讀的警告等於沒有警告。
不確定時的預設值:對最低標準的部分阻止,對其他部分前兩週僅警告。
Q3: 你心裡有目標數字了嗎?還是我先測量你現在的狀況,然後守住那條線?
猜測:先測量。大多數團隊沒有具體數字,而憑空想出來的數字通常會被忽略。
不確定時的預設值:測量並守住。請見下方「棘輪機制」。
Q4: 在代理程式交出成果之前,你能夠忍受的最慢檢查是多長時間?
猜測:大約 90 秒。再長的話你就會停止執行它。
不確定時的預設值:任務結束時 90 秒,CI 中則不限制。
問完四個問題就停止。一個有十二個問題的訪談問卷只會產出一個沒人看得懂的設定,以及一個後悔開始的使用者。
步驟 3:撰寫 CONSTRAINTS.md
一份檔案放在專案根目錄。任何代理程式在任何框架上都能讀取它,而對它的修改會在 code review 中顯示出來,這是它應在的位置。
# Constraints
最後審查:2026-08-08 由 @mukhon-core
## 最低標準(總是強制執行,無需設定)
- 不得新增任何抑制註解:`@ts-ignore`, `eslint-disable`, `# noqa`, `# type: ignore`
- 不得有任何未實作殘留:`throw new Error("Not implemented")`, 空的 `catch {}`
- 不得跳過或刪除測試,除非在 commit 訊息中說明理由
- 不得在原始碼中包含機密
- 此檔案不得為了讓某個變更通過而被削弱
## 以數字強制執行
| 維度 | 規則 | 由誰檢查 | 何時執行 |
|------|------|---------|---------|
| 型別 | 零型別錯誤 | `tsc --noEmit` | 每次編輯 |
| 語法 | 根據我們的設定檔,零錯誤 | `biome check` | 每次編輯 |
| 機密 | 原始碼中無機密 | `gitleaks detect --redact` | 每次編輯 |
| 覆蓋率 | 修改行數 ≥ 80% 覆蓋 | `vitest run --coverage` + git diff | 任務結束,CI |
| 安全性:程式碼 | 無高風險發現 | `semgrep scan --config p/default` | CI |
| 安全性:相依套件 | 無高風險(含)以上 | `osv-scanner scan source -r .` | CI |
| 無障礙性 | 零關鍵或嚴重問題 | `axe $PREVIEW_URL --tags wcag2a,wcag2aa,wcag21aa` | Preview 部署 |
| 效能 | LCP ≤ 2500ms, CLS ≤ 0.1 | `lighthouse $PREVIEW_URL --output=json` | Preview 部署 |
每一行都指名了產出結果的命令。一個有數字但沒有命令的維度只是個願望,而非約束條件。
## 已測量,尚未強制執行
| 指標 | 目前值 | 方向 |
|------|--------|------|
| 專案總覆蓋率 | 62.4% | 不得下降 |
| 打包大小(主分支) | 184 kB | 不得增長 |
## 例外
| ID | 規則 | 路徑 | 原因 | 負責人 | 到期日 |
|----|------|------|------|--------|--------|
| W1 | `no-explicit-any` | `src/legacy/**` | 重構追蹤於 ENG-441 | @mukhon-core | 2026-11-01 |
然後在 AGENTS.md 和 CLAUDE.md 中加入一行:在撰寫程式碼之前,請閱讀 CONSTRAINTS.md。請勿為了讓某個變更通過而削弱其內容。
步驟 4:安裝每個維度所需的工具
選擇一個維度就意味著要安裝某些東西。不要讓使用者只有一個數字卻沒有實現機制,也不要自己發明檢查工具,當業界已有事實上的標準工具時——這裡列出的工具之所以被選中,是因為它們的規則格式和門檻值是生態系中其他工具的目標,因此團隊現有的設定檔可以繼續運作。
| 維度 | 工具 | 安裝方式 | 執行命令 | 關卡條件 |
|---|---|---|---|---|
| 型別 (TS) | tsc | 已存在 | tsc --noEmit | 任何錯誤 |
| 型別 (Python) | mypy | pip install mypy | mypy . | 任何錯誤 |
| 語法 | 你現有的設定檔 | 已存在 | eslint . / biome check / ruff check | 任何錯誤 |
| 覆蓋率 (JS) | 你的測試框架 | 已存在 | vitest run --coverage (或 jest --coverage) | 修改行數的覆蓋率 |
| 覆蓋率 (Python) | pytest-cov | pip install pytest-cov | pytest --cov --cov-report=lcov | 同上 |
| 安全性:程式碼 | Semgrep | pipx install semgrep | semgrep scan --config p/default --config p/owasp-top-ten | 任何高風險發現 |
| 安全性:機密 | gitleaks | brew install gitleaks | gitleaks detect --redact --no-banner | 任何發現 |
| 安全性:相依套件 | osv-scanner | brew install osv-scanner | osv-scanner scan source -r . | 高風險(含)以上 |
| 效能:頁面 | Lighthouse | npm i -D lighthouse | lighthouse $URL --output=json --quiet | LCP, CLS, 效能分數 |
| 效能:打包 | size-limit | npm i -D size-limit | size-limit --json | 每個進入點的大小預算 |
| 無障礙性 | axe-core | npm i -D @axe-core/cli | axe $URL --tags wcag2a,wcag2aa,wcag21aa | 零關鍵或嚴重問題 |
| 架構 | dependency-cruiser | npm i -D dependency-cruiser | depcruise --validate src | 任何違規 |
| 斷言語法品質 | Stryker | npm i -D @stryker-mutator/core | stryker run --mutate <changed files> | 突變測試分數 |
有五件事如果你跳過會很麻煩:
- gitleaks 的
--redact參數不是可有可無的。 沒有它,匹配到的機密會出現在代理程式的對話記錄中,這就是金鑰如何洩漏到 log、摘要或 commit 訊息中的方式。回報規則和位置,永遠不要回報實際值。 - Lighthouse 和 axe 需要一個 URL。 它們只能在正在執行的應用程式上運作,因此它們應屬於執行階段——對 Preview 部署或你先啟動的本地伺服器執行。如果專案沒有可訪問的 URL(例如 CLI 工具、函式庫、桌面應用程式),請說明並放棄該維度,而不是發明一個無法執行的檢查。
- 將耗時的檢查範圍限定在 diff 內。
stryker run --mutate對整個儲存庫執行需要數小時,最終會被關掉;但只對變更觸及的檔案執行則不到一分鐘。Semgrep 也是同樣,它接受路徑清單。 - 覆蓋率不需要第二次測試執行。 讀取你的測試套件已經產出的 lcov 報告,並與
git diff進行交集。為了得到一個數字而執行兩次測試套件,是最快讓人討厭這個機制的方法。 - Semgrep 的註冊表規則是免費執行的;但在重新分發前請檢查授權條款。
opengrep是一個可以直接替換的分支,具有相同的規則格式和 JSON 輸出,如果你的法務團隊有相關顧慮的話。
將每個檢查加入專案自己的腳本中,以便在沒有代理程式的情況下也能重現:
{
"scripts": {
"check:fast": "tsc --noEmit && eslint . && gitleaks detect --redact --no-banner",
"check:task": "npm run check:fast && vitest run --coverage",
"check:full": "npm run check:task && semgrep scan --config p/default && osv-scanner scan source -r ."
}
}
這個對應關係比工具本身更重要。check:fast 在每次編輯後執行,check:task 在代理程式認為工作完成時執行,check:full 在 CI 中執行。
現在命令存在於兩個地方——CONSTRAINTS.md 的「由誰檢查」欄位和這些腳本中。CONSTRAINTS.md 是權威來源:它在每個命令旁邊附帶了理由,並會在 code review 中顯示。腳本只是方便的包裝器,必須與之對應,而不是第二個真相來源;如果兩者出現分歧,以檔案為準。
步驟 5:將檢查掛接到開發生命週期
最常見的錯誤是在所有地方執行所有檢查。一個會拖慢代理程式的檢查會被關掉,而被關掉的關卡比沒有關卡更糟,因為表面上看起來門檻仍然存在。
| 階段 | 命令 | 執行的檢查 | 時間預算 |
|---|---|---|---|
| BUILD | /build | 型別、語法、機密、最低標準 | 5 秒內,僅限修改的檔案 |
| VERIFY | /test | 相關測試、修改行數的覆蓋率 | 90 秒內 |
| REVIEW | /review | 全部,加上下方的守門機制 | 數分鐘 |
| SHIP | /ship | 方向檢查,無回歸 | CI |
兩條規則可以讓這個流程變得可接受:
- 範圍限定在 diff 內。 檢查此次變更觸及的行數,而非整個儲存庫。修改行數的覆蓋率是代理程式可以改變的數字;而專案總覆蓋率是它繼承來的。
- 成本決定放置位置。 任何耗時超過幾秒的檢查都應移出編輯循環。對整個儲存庫進行突變測試需要數小時;但對變更觸及的檔案執行則不到一分鐘,這決定了檢查是會被人執行還是被忽略。
步驟 6:守護合約本身
會有人指出:如果代理程式既寫程式碼又寫檢查,那檢查就證明不了什麼。這話只對了一半,而且值得在工程層面加以防範。
代理程式不會精心設計漏洞。它們只是遇到紅燈,然後選擇最便宜的路線變綠。在 code review 時,請關注 diff 中的這五種行為:
- 門檻被移動了。 預算被調低、嚴重性被降低、某個檢查從快速階段被移除。請將
CONSTRAINTS.md與其在分支點時的狀態進行比較。 - 測試變簡單了。 加入了
.skip、刪除了測試檔案、或從仍然執行的測試中移除了斷言。 - 檢查器被靜音了。 新增了
@ts-ignore或eslint-disable。有四種抑制註解需要特別注意,因為它們關閉了你信賴的檢查:istanbul ignore從覆蓋率中排除程式碼而不是測試它,Stryker disable隱藏了存活的突變體,nosemgrep和gitleaks:allow對安全性發現做同樣的事。 - 工作未完成。 一個拋出錯誤的殘留 stub、一個將失敗轉為靜默的空
catch、一個取代了應有實現的TODO。 - 出現了例外。 例外表格中出現了一個沒有人討論過的新列。
這些都不需要比 git diff 更複雜的工具。強化門檻應該默默進行;削弱門檻則應該大聲喧嘩。
與有數字的維度不同,最低標準沒有自己的業界標準工具,因此要求代理程式執行它時,代理程式往往會自己從頭寫一個檢查器,而兩個代理程式會寫出兩個不同的版本。此技能附帶了一個這五項檢查的參考實作,位於 references/floor-guard.md(範圍限定於 diff,回傳碼 0/1/2,模式可根據生態系統調整)。請改編該實作,而非重新發明輪子,理由與每個維度都指名一個業界標準工具相同:確保機制在不同執行環境和技術棧之間保持一致。
並非所有檢查都具有相同的循環性。 根據一個問題對它們進行排序:代理程式能否透過撰寫無效的程式碼來通過這項檢查?
- 外部——axe-core 實現了 WCAG 標準、
osv-scanner讀取漏洞資料庫、Lighthouse 在真實瀏覽器中測量。代理程式無法反駁這些。 - 專案——你的 lint 規則、你的層級邊界。由人類維護的檔案。
- 套件——你自己的測試。最有用,也是唯一真正循環的檢查。
一個代理程式可以在空函式上取得 100% 的覆蓋率。它無法在空函式上通過 axe 的檢查。在每一個維度中,盡可能依賴外部檢查器,因為它們的判斷標準是固定的,不依賴於代理程式是否選擇誠實。
棘輪機制
測量並守住。這是這裡最危險的預設值,所以值得明確說明。
當你說「測量當前值並守住那條線」時,你可能會得到一個比理想值更糟的數字,而且永遠無法改善它。代理程式唯一能通過的路徑就是寫出剛好達到門檻的程式碼,而不是超越它。
解決方法: 在 CI 中每週更新一次基準值。
CONSTRAINTS.md 中「已測量,尚未強制執行」的表格就是為此設計的。方向 欄位定義了什麼是進步(「不得下降」、「不得增長」),而 CI 管道中的一個排程工作會測量這些數字並更新它們。門檻只會往一個方向移動。
這會產生與大多數人預期相反的效果:一個逐漸被接受的低門檻會開始上升,因為真正的侷限是倉庫的歷史,而不是你第一次測量時的運氣。
底線:最低標準
即使使用者不想回答任何問題,這部分也總是存在。一個小於「最小值」的世界。
如果缺少 CONSTRAINTS.md 且在非互動式環境中(CI、/loop、自動化執行),或在互動式環境中使用者表示「現在就開始」而未回答問題,請套用此最低標準。
# Constraint:最低標準
已自動套用,因為缺少 CONSTRAINTS.md 且無法(或使用者選擇不)進行訪談。
## 強制執行規則
1. 不得新增抑制註解:`@ts-ignore`, `eslint-disable`, `# noqa`, `# type: ignore`
2. 不得有未實作殘留:`throw new Error("Not implemented")`, `catch {}`
3. 不得跳過或刪除測試
4. 原始碼中不得有機密
這是代理程式可以在無人介入的情況下自行執行的內容,核心想法是「不要讓事情變得更糟」。這不是一個好的合約,但比沒有合約好。