約束驅動開發
概觀
此套件中的其他技能描述了「良好」的樣貌。code-review-and-quality 提供了五個面向。test-driven-development 提供了一個循環。security-and-hardening 提供了威脅清單。這些都只存在於 Agent 閱讀、且可能不會遵循的文字中,而且它們都無法在會話結束後存留。
此技能產出了不同的東西:一份關於此專案標準的書面記錄,附有數字,會比對話更長久,並且可以機械式地檢查。
這點很重要。當你編寫程式碼時,閱讀它就能知道它好不好。而一個 Agent 一個下午產出的程式碼,比你一週內能讀的還多,因此判斷的責任必須從你的腦袋,轉移到能在循環中執行的檢查上。這些檢查必須存在,它們需要你真正選擇的數字,而且它們必須在足夠接近工作的地方觸發,以便 Agent 能夠修正自己的輸出。
規格驅動開發(Spec-driven development)說明了要建置什麼。測試驅動開發(Test-driven development)證明了它能運作。而約束驅動開發(Constraint-driven development)則是在任何人在 Pull Request 中爭論之前,就定義了什麼叫做「好到可以發佈」。
何時使用
在以下情況應用此技能:
- 啟動一個新專案或一個重大功能,且沒有任何書面的品質標準
- 使用者要求「設定約束」、「增加品質閘門」、「定義我們的標準」,或「阻止 Agent 產出垃圾」
- Agent 產出大量程式碼,但沒有人會逐行審視
- CI 有檢查,但沒有人能說哪些會阻擋合併,哪些只是裝飾
- 覆蓋率、效能或無障礙性數字每次 PR 都在爭論,而不是一次決定好
- 你即將執行
/build auto或任何自主循環,且擋在主線與它之間的唯一事物,是一個同一個 Agent 也寫了的測試套件
何時不要使用:
- 專案已有
CONSTRAINTS.md且使用者未修改它——請直接閱讀並遵循它 - 一次性腳本、概念驗證專案、可拋棄原型
- 使用者現在需要程式碼審查(使用
code-review-and-quality)或建立 CI Pipeline(使用ci-cd-and-automation) - 預期生命週期僅兩週、產品市場契合前的程式碼——最低標準仍然值得做,但其他部分則否
載入約束
訪談需要一個真實的使用者。請勿在非互動式上下文(CI、/loop、自主執行)中執行。如果約束條件缺失且你處於上述情境,請套用下方的最低標準,記錄你已執行此操作,並將其餘部分標記給真人處理。
流程
步驟 1:在提問前先偵測
永遠不要問你可以讀取到的東西。在第一個問題之前,請收集以下資訊:
| 項目 | 查看位置 |
|---|---|
| 語言與技術棧 | package.json, pyproject.toml, go.mod, Cargo.toml |
| 測試執行器 | 開發依賴、test 腳本、現有測試檔案 |
| 現有 Linter | eslint.config.*, biome.json, .ruff.toml |
| 目前的覆蓋率 | coverage/ 輸出,或執行一次測試套件 |
| CI | .github/workflows/, .gitlab-ci.yml |
| Agent 框架 | .claude/, .codex/, AGENTS.md |
用兩行回報你的發現,然後只問剩下的問題。
步驟 2:四個問題,每個都附帶預設值
遵循 interview-me 中的一次一個問題的原則,但有一個改變:這裡的每個問題都有預設值,所以「我不知道」是一個完整的答案,仍然能產出一個可運作的設定。
Q1:除了最低標準,你希望強制執行以下哪些項目?
(a) 新程式碼的測試覆蓋率
(b) 安全性掃描
(c) 效能預算
(d) 無障礙性
(e) 架構邊界
猜測:(a) 和 (b)——你已經有測試執行器,並且正在處理使用者輸入。
如果不確定,預設值:(a) 和 (b)。
說明每個選擇的成本:(c) 和 (d) 需要一個運作中的 URL,(e) 需要編寫規則檔案。
Q2:當 Agent 在任務進行中時,某項檢查失敗,它應該封鎖還是僅發出警告?
猜測:封鎖。你在無人監控下執行 Agent,而一個沒人看的警告就只是個警告。
如果不確定,預設值:最低標準項目封鎖,其餘項目在前兩週僅發出警告。
Q3:你心中有目標數字,還是希望我衡量你今天的狀況,然後維持那個標準?
猜測:衡量。大多數團隊沒有數字,而憑空發明一個數字會被忽略。
如果不確定,預設值:衡量並維持。請參閱下方的「棘輪機制」。
Q4:在 Agent 交回工作成果之前,你能忍受的最慢檢查時間是多久?
猜測:大約 90 秒。再長的話,你就不會再執行它了。
如果不確定,預設值:任務結束時 90 秒,CI 中則不限時。
停在四個問題。一個十二題的問答會產生一個沒人懂的設定,和一個後悔開始的使用者。
步驟 3:撰寫 CONSTRAINTS.md
一個位於版本庫根目錄的檔案。任何 Agent 在任何框架上都可以讀取它,而對它的修改會在審查中出現在它該在的地方。
# Constraints
最後審閱:2026-08-08 由 @addy
## 最低標準(永遠強制執行,無需設定)
- 無新的抑制註解:`@ts-ignore`, `eslint-disable`, `# noqa`, `# type: ignore`
- 無未實作的存根:`throw new Error("Not implemented")`, 空的 `catch {}`
- 未在 commit 訊息中說明原因時,不得跳過或刪除測試
- 原始碼中不得有密碼
- 此檔案不得為了讓某項變更通過而遭到弱化
## 以數字強制執行
| 維度 | 規則 | 檢查者 | 執行時機 |
|-----------|------|-----------|---------|
| 型別 | 零型別錯誤 | `tsc --noEmit` | 每次編輯 |
| Lint | 根據我們的設定,零錯誤 | `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` | 預覽佈署 |
| 效能 | LCP ≤ 2500ms, CLS ≤ 0.1 | `lighthouse $PREVIEW_URL --output=json` | 預覽佈署 |
每一行都指名了產生判決結果的指令。一個維度如果有數字但此欄位沒有指令,那就只是一個願望,而不是一個約束。
## 已測量,尚未強制執行
| 指標 | 今日數值 | 方向 |
|--------|-------|-----------|
| 專案覆蓋率 | 62.4% | 不得下降 |
| 套件大小(主線) | 184 kB | 不得增長 |
## 例外情況
| ID | 規則 | 路徑 | 原因 | 負責人 | 到期日 |
|----|------|------|--------|-------|---------|
| W1 | `no-explicit-any` | `src/legacy/**` | 重構追蹤於 ENG-441 | @addy | 2026-11-01 |
然後在 AGENTS.md 和 CLAUDE.md 中加入一行:在編寫程式碼之前,請先閱讀 CONSTRAINTS.md。請勿為了讓變更通過而弱化它。
步驟 4:安裝每個維度所需的工具
選擇一個維度意味著要安裝某個東西。不要留給使用者一個數字卻沒有機制,也不要在已有事實上的標準檢查工具時自行發明檢查器——列出這些工具的原因是其規則格式和閾值是生態系統中其他一切工具所針對的目標,這樣團隊現有的設定才能持續運作。
| 維度 | 工具 | 安裝 | 執行 | 閘門條件 |
|---|---|---|---|---|
| 型別 (TS) | tsc | 已存在 | tsc --noEmit | 任何錯誤 |
| 型別 (Python) | mypy | pip install mypy | mypy . | 任何錯誤 |
| Lint | 你現有的設定 | 已存在 | 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 <變更的檔案> | 突變測試分數 |
五件如果你跳過就會出問題的事:
- gitleaks 的
--redact不是選配。 沒有它,比對到的密碼會出現在 Agent 的記錄中,這就是洩漏的金鑰最後如何出現在日誌、摘要或 commit 訊息中。請回報規則和位置,永遠不要回報其值。 - Lighthouse 和 axe 需要一個 URL。 它們只能在執行中的應用程式中運作,因此它們屬於執行階段,針對預覽佈署或你首先啟動的本機伺服器。如果專案沒有可以存取的 URL——像是 CLI、函式庫、桌面應用程式——請說明情況並放棄該維度,而不是發明一個無法執行的檢查。
- 針對耗時的檢查,請將範圍限定在 diff 上。 對整個倉庫執行
stryker run --mutate需要數小時,最終會被關閉;而只對變更觸及的檔案執行則只需不到一分鐘。Semgrep 也是如此,它可以接受一個路徑列表。 - 覆蓋率不需要第二次測試執行。 讀取你的測試套件已經寫入的 lcov 檔案,然後與
git diff交集。為了得到一個數字而執行測試套件兩次,是最快讓人們討厭這件事的方法。 - Semgrep 的 registry 規則是免費執行的;但在重新分發前請檢查其授權。 如果你的法務團隊在意,
opengrep是一個具有相同規則格式和 JSON 輸出的直接替代分支。
將每個檢查加入專案自身的腳本,使其無需 Agent 即可重現:
{
"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 在 Agent 認為工作完成時執行,check:full 在 CI 中執行。
這些指令現在存在於兩個地方——CONSTRAINTS.md 中的「檢查者」欄位,以及這些腳本。CONSTRAINTS.md 是權威來源:它承載了每個指令背後的原因,並且會在審查中出現。腳本是必須與之同步的便利包裝,而非第二個事實來源;如果它們偏離了,以檔案為準。
步驟 5:將其連接到生命週期
最常見的錯誤是在所有地方執行所有檢查。一個會拖慢 Agent 的檢查會被關閉,而一個被關閉的閘門比沒有閘門更糟,因為標準看起來仍然存在。
| 階段 | 指令 | 執行的項目 | 預算 |
|---|---|---|---|
| BUILD | /build | 型別、lint、密碼、最低標準 | 小於 5 秒,僅限變更的檔案 |
| VERIFY | /test | 相關測試、變更行的覆蓋率 | 小於 90 秒 |
| REVIEW | /review | 所有項目,加上下方的護欄 | 數分鐘 |
| SHIP | /ship | 方向檢查,無倒退 | CI |
兩個讓這一切保持可接受的規則:
- 範圍限定在 diff。 檢查此變更觸及的行,而非整個倉庫。變更行的覆蓋率是一個 Agent 可以改變的數字;而專案覆蓋率是它繼承來的。
- 成本決定擺放位置。 任何超過數秒的檢查都應移出編輯循環。對整個倉庫進行突變測試需要數小時;對變更觸及的檔案進行則只需不到一分鐘,這就是一個檢查會被人執行與不會被執行的區別。
步驟 6:守護標準本身
有人會指出,如果 Agent 同時寫了程式碼和檢查,那麼檢查就無法證明什麼。對了一半,而且值得為此進行工程設計。
Agent 不會精心設計巧妙的漏洞。它們遇到紅燈時,會選擇通往綠燈最便宜的路徑。在審查時,請留意 diff 中的這五種動作:
- 閾值變動了。 預算降低、嚴重性調降、檢查從快速階段被移除。將
CONSTRAINTS.md與其在分支點時的狀態進行比較。 - 測試變容易了。 新增了
.skip、測試檔案被刪除、從保留的測試中移除了斷言語句。 - 檢查器被靜音了。 新增了
@ts-ignore或eslint-disable。有四個抑制註解需要特別注意,因為它們會關閉你依賴的檢查:istanbul ignore會讓程式碼不被覆蓋,而不是測試它;Stryker disable會隱藏存活的突變體;nosemgrep和gitleaks:allow則用於安全性發現。 - 工作未完成。 會拋出錯誤的存根、將失敗轉為靜默的空的
catch、一個TODO取代了應該有實作的地方。 - 出現了一個例外。 例外情況表格中出現了一個未經討論的新行。
這些都不需要比 git diff 更複雜的工具。收緊標準應該悄無聲息;放寬標準則應該大聲喧嘩。
與有數字的維度不同,最低標準沒有它自己的事實上的工具,因此一個被要求強制執行它的 Agent 傾向於從頭開始編寫檢查器,而兩個 Agent 會寫出兩個不同的檢查器。此技能附帶了這五項檢查的參考實作,位於 references/floor-guard.md(範圍限定在 diff,退出碼為 0/1/2,模式可根據生態系統調整)。請調整該實作而非重新發明,原因與每個維度都指名一個事實上的工具相同:這樣機制才能在跨執行和技術棧中保持一致。
並非所有檢查都是同等循環論證的。 用一個問題來排序它們:Agent 可以透過編寫無法運作的程式碼來讓此檢查通過嗎?
- 外部——axe-core 編碼了 WCAG,
osv-scanner讀取漏洞資料庫,Lighthouse 測量真實瀏覽器。Agent 無法與這些爭論。 - 專案——你的 lint 規則、你的層級邊界。檔案由真人擁有。
- 套件——你自己的測試。最有用的,而且是唯一真正循環論證的。
一個標準
…
[因翻譯限制而截斷]