模組二|工程底座與可驗收的 AI(Day 5–9)
昨天講座標系那條規則只寫了一句話就撐住 44 個檔案。今天把那句話所在的檔案整份打開。
這個 repo 的根目錄有兩份給 AI 讀的合約:AGENTS.md 69 行、CLAUDE.md 139 行。兩份都是我自己寫的,內容幾乎不重疊,維護軌跡差得更遠——AGENTS.md 在 08-06 03:36 進 repo 之後再也沒有被改過,全歷史只有一個 commit;CLAUDE.md 從專案的第一個 commit 就存在,前後 10 個 commit 碰過它,從 68 行長到 139 行。
結論先講:一份合約有沒有用,看它有沒有把「不准做什麼」寫成具名清單。形容詞式的規則(「保持風格一致」「不要過度耦合」)在 AI 這裡等於沒寫,因為它無法判定自己有沒有違反。
用 AI 寫程式最反直覺的一點:它不會記得你上次講過什麼。你昨天花十分鐘解釋為什麼這個專案不用 TypeScript,今天開新對話,它照樣可能給你一個 .ts 檔。
這不是模型笨。這是無狀態。而無狀態的系統要靠什麼維持一致性——把狀態外部化,放進它每次都會讀的地方。
不同工具讀不同檔名:Codex 讀 AGENTS.md,Claude Code 讀 CLAUDE.md。檔名是慣例,內容的結構是共通的。
AGENTS.md 全檔只有五個 ## 標題。以下把 69 行壓成骨架,Project 與 Commands 兩段是原文,另外三段的內容用括號標示:
## Project
This repository is a JavaScript browser game named `save-the-dog-web`.
Core stack: Vite / PixiJS 8 / Matter.js 0.20 / Howler.js / Vitest / Playwright
Do not introduce TypeScript or a frontend UI framework such as React, Vue,
Svelte, or Angular.
## Commands
Before finishing a code task, run:
- `npm run lint`
- `npm run test:unit -- --run`
- `npm run build`
When a task changes rendering, browser behavior, or SVG assets, also run:
- `npm run assets:validate`
- `npm run test:e2e`
## SVG Rules (八條 must + 一條 never,見下節)
## Engineering Rules(七條 bullet)
## Definition Of Done
Report: files added/changed/deleted, commands executed, lint/test/validation/
build results, remaining manual review items.
Do not declare a task complete when required checks fail.
五塊各自防一件事:
| 區塊 | 不寫會發生什麼 |
|---|---|
| Project | AI 引進不該有的框架,或用它熟悉的技術棧改寫你的 |
| Commands | 交出來的東西根本不能建置 |
| SVG Rules / Engineering Rules | 每一輪的寫法都不一樣 |
| Definition Of Done | 只跟你說「做好了」,你得自己去查它做了什麼 |
最後一塊常被當成客套話,我認為它是這五塊裡投報率最高的。Definition Of Done 要求的四欄——動了哪些檔、跑了哪些指令、四項檢查各自的結果、還剩下哪些需要人工目視——把「我做好了」這句話拆成四個可以被反駁的欄位。少了這一塊,你收到的回報會是一段流暢的自然語言摘要,而你得自己去 git status 和 CI 裡把真相挖出來。順帶一提,整份 AGENTS.md 沒有一句在教怎麼寫程式,六十九行全部是邊界與交付格式。
還有一個實務細節值得單獨講:合約裡的指令必須真的能跑。 AGENTS.md 點名六個 npm script(lint、test:unit、build、assets:validate、test:e2e、preview),package.json 的 scripts 一共七項,六個全在裡面【實查】。寫了一個不存在的指令,AI 跑失敗之後最可能的行為是跳過它然後宣告完成——你等於自己給了它一個豁免。
這個專案有一份很少見的對照組:PRD.md 的〈Codex 專案規範〉一節裡,內嵌了一份 64 行的 AGENTS.md 草稿(PRD.md:190-255 的程式碼區塊)。那份草稿在專案的第一個 commit 就存在(b4c2c62,03:07,當時檔名還是 deep-research-report.md,03:12 才更名為 PRD.md),實際的 AGENTS.md 是 29 分鐘後的 1dd9971(03:36)。兩份長度差不多,內容的可判定性差很多。
| 草稿寫的 | 實際寫的 | 差在哪 |
|---|---|---|
Do not introduce TypeScript or a frontend UI framework. |
...such as React, Vue, Svelte, or Angular. |
從類別變成具名清單 |
contain semantic group IDs |
contain all required semantic <g id="..."> groups from docs/asset-inventory.md |
從形容詞變成「去哪張表查」 |
use only colors defined in docs/art-style.md |
同上,另加 plus none and currentColor |
補例外,否則合法檔案會被誤判 |
avoid complex filters unless explicitly requested |
整條刪掉 | 「複雜」判定不了 |
Never replace... 列 5 項 |
列 10 項 | 見下節 |
Do not directly couple scenes to Matter.js internals. |
keep synchronization one-way from Matter bodies to Pixi display objects |
從「不要耦合」變成「同步只准往一個方向」 |
| (無) | Game logic uses the 750 x 1334 internal coordinate system from PRD.md. |
新增 |
第一列是整張表最值錢的一列。「不要引進前端框架」是人話,AI 讀得懂,但你沒辦法用它寫出一條檢查。改成點名 React、Vue、Svelte、Angular 之後,它就能一比一地變成 eslint.config.js:22-50 的 no-restricted-imports——paths 列 react/react-dom/vue/svelte,patterns 列 @angular/*,跟合約那句話的清單完全對應【實查】。同一段的「不引進 TypeScript」也一樣,變成 35 行的 scripts/check-no-typescript.js,掛在 npm run lint 的第一段。
精確不是為了嚴格,是為了讓規則有機會離開文件、變成程式。

我把那十扇看起來都很合理的小門,一扇一扇栓起來
AGENTS.md 的 ## SVG Rules 全段是這樣(:33-48):
## SVG Rules
SVG asset requirements are enforced by `scripts/validate-svg.js` and defined by
`docs/art-style.md` plus `docs/asset-inventory.md`.
Every SVG must:
- contain `xmlns="http://www.w3.org/2000/svg"`
- contain the exact requested `viewBox`
- use `preserveAspectRatio="xMidYMid meet"`
- contain all required semantic `<g id="...">` groups from `docs/asset-inventory.md`
- use only colors defined in `docs/art-style.md`, plus `none` and `currentColor`
- keep all visible geometry inside the `viewBox`
- keep at least 6% safe padding on all sides
- avoid text elements and non-ASCII visual characters
Never replace a requested SVG with emoji, Unicode symbols, CSS shapes, remote
URLs, Base64 images, `image` / `script` / `foreignObject` elements, event
attributes, or external fonts.
最後那一句,是從草稿到定稿改動幅度最大的一處:草稿列 5 項,定稿列 10 項。它列的十樣東西不是隨便想的,是卡住時的十條捷徑:
| 捷徑 | 它為什麼看起來合理 |
|---|---|
| 塞一個 emoji | 「反正是佔位圖,之後再換」 |
| 內嵌一張 Base64 PNG | 「畫不出漸層,但至少畫面出得來」 |
| 引用一個遠端字型或圖片 URL | 「這個圖示網路上有現成的」 |
用 <rect> + <circle> 拼形狀 |
「這就是 SVG 啊,沒有違規」 |
寫 <text> 當圖示 |
「文字也是視覺元素」 |
每一條單看都能自圓其說,合起來就是專案風格徹底崩掉。這就是「不准做什麼」比「要做什麼」重要的原因:正面表述只描述了一個目標,反面表述封掉的是達不到目標時的所有出口。
需要誠實標一件事:這十項裡面,有一項到今天都沒有自動檢查在擋,而且我認為它天生就擋不住。是哪一項、為什麼,Day 8 那篇會用驗證器的原始碼講完。
CLAUDE.md 管的是另一件事:不要順手改回去
這根柱子是我故意歪的,所以我得把後果一起掛上去
如果 AGENTS.md 是「進門守則」,CLAUDE.md 就是「這棟房子為什麼有些地方長得很怪」。
它的比重可以量:全檔 18 個「不要」、6 個「不得」、3 個「不可」、12 個「必須」【實測:grep -oE '不得|不要|必須|不可' CLAUDE.md | sort | uniq -c】。而這些「不要」大部分不是禁止犯錯,是禁止把一個刻意的決定改回直覺版本:
PhysicsManager預設開啟enableSleeping,不要關掉:靜止的防線會被 Matter 的位置解算持續灌回能量而永遠搖擺(實測 10 秒漂移 ±250 px)⋯⋯
節流用 wall clock 是刻意的例外。 專案的通則是時間走
updateFixed⋯⋯AudioSystem裡有註解說明,不要「順手修正」成固定步長。
難度靠開口(寬度、深度、位置、數量)、洞內障礙與畫線上限推進,不要調快蜜蜂。
沒達到全勝就改關卡幾何,不要放寬測試。
這一類條目的共同形狀是:「規則 + 違反它會發生的具體現象」。單寫「不要關掉 enableSleeping」,下一輪 AI(或三個月後的我)看到 sleeping 造成的怪行為,會覺得關掉很合理。加上「10 秒漂移 ±250 px」,那條路就被關掉了。
這也解釋了為什麼兩份檔案的維護節奏差這麼多:AGENTS.md 寫的是開工前就知道的事,所以寫完一次就定了;CLAUDE.md 寫的是撞到之後才知道的事,所以它跟著專案長。可以逐個 commit 量給你看:04:58 時它是 76 行,13:22 變 90 行、14:58 變 108 行、15:45 變 122 行、18:40 定在 139 行。63 行是玩法開始實作之後才補進去的,而那 63 行幾乎全是「這裡踩過,別再踩」。
代價也一起攤開:沒有人在維護 AGENTS.md,所以它現在有一條已經過期的條款。 :58 寫著「Keep foundation changes free of gameplay initialization until the render foundation change」——那個「render foundation change」在 08-06 04:08 的 314a7b4 就完成了,這條規則從那之後就沒有作用,但它還躺在合約裡。:57 的「Random gameplay behavior must use the project seeded random utility once it exists」也一樣,那個「once it exists」的條件早就滿足了。
合約會過期,而過期的合約比沒有合約更難察覺,因為它看起來還在生效。
最後講一句誠實話,這句話是明天之後兩篇的入口。
寫在合約裡但沒有自動檢查的規則,我沒有辦法量它的違反率——這個專案沒有留下被擋下產出的紀錄,我不會憑印象編一個數字。我能說的是可查的那一面:AGENTS.md 裡的規則,有些能一比一變成 lint 規則或驗證器規則(今天示範了兩條),有些一條都寫不出來。這兩類的分界線在哪裡、佔比各是多少,是 Day 8 開頭與 Day 29 收尾的主題,今天不展開。
只留一句:合約管的是「AI 知道該做什麼」,檢查器管的是「AI 做錯了會被擋下」。只有前者,等於只有交通號誌沒有測速照相。
合約的可執行性,等於它裡面具名清單的比例。凡是用形容詞寫的規則,都只是給模型的背景資訊,不是約束。
三件今天就能做的事:
明天 Day 7 換一個更具體的場景:三十幾張 SVG 要分好幾批交給 AI 生成,怎麼讓它們看起來像同一個人畫的?答案跟今天同一條線——先訂命名。這個專案的做法是三層約束加一套母版制,而其中最反直覺的一層,目的不是為了讓素材好編輯,是為了讓「風格一致」這件事變成可以被程式檢查的東西。
本篇數字的快照時間:2026-08-07 12:35(+0800),對應 commit
5aa3705。專案仍在開發中,量體數字會變動;引用的每一項都可以用本文提到的檔案路徑與行號自行對照。
可玩網址:https://save-the-dog-web.vercel.app/|原始碼:https://github.com/HarryFan/save-the-dog-web
如果你卡在語法
深入原理