昨天讓 OpenSpec 拆了一份舊需求。今天真正開工,要的東西其實都在了:規格、15 條非目標、五條硬規則和它們的裁判,開賽前就寫好在 repo 裡。缺的只有一樣 —— 這十天怎麼排。
我先排了一張表(8 月下旬排的,在工作區裡)。今天讓另一個工具也排一張,然後拿兩張對。工具是 GitHub 的 spec-kit(v1.0.6,9/10 才發),跟昨天的 OpenSpec 是兩種東西:OpenSpec 管一個 change;spec-kit 的 constitution 管整個專案,specify、plan、tasks 原本以一個 feature 為單位。這次把它拿來對整個活動報名系統規劃一輪。它的正式流程是 constitution → specify → plan → tasks → implement,clarify 是可選的一步;故意把它加進來,實際跑五步:constitution → specify → clarify → plan → tasks。後面的步驟都吃前面的產出,而且 plan 那步會拿 constitution 逐條檢查設計。昨天說「一個人三十天的新專案不用 OpenSpec」,講的是 change 級的記錄成本;今天要的是專案級的一張工作清單,正好是另一件事。
規則只有一條:規格不准擴張。它只能把我寫的東西變成它的格式,不能替我加。遇到空白,不准自己填 —— 標出來問我,由我決定。
| 步驟 | 輸入 | 產出 |
|---|---|---|
constitution |
CLAUDE.md 五條硬規則、15 條非目標、「做完」定義 —— 照抄,不准新增 | 一份 constitution v1.0.0。想加的規則它列在最後「建議新增(未採納)」 |
specify |
docs/spec.md + docs/non-goals.md,一字不擴張,模糊處全部標出來、不准自己決定 |
364 行、52 條功能需求(依 endpoint 分組,每組從新的十位數起編,所以號碼排到 FR-081) —— 和 21 個 [NEEDS CLARIFICATION] |
clarify |
21 條的答案寫成一份檔一次餵進去,原則是「市場上最常見的做法」 | 標記清零,外加一個答案之間的矛盾、兩處與原文的衝突 |
plan |
技術棧鎖死(Workers + Hono + D1 + Pages + SwiftUI + 一份 OpenAPI),五條規則逐條 gate | 五條都過,但四個 ⚠️;一份 OpenAPI 契約(18 條、21 個 schema) |
tasks |
每個 task 附建議日期:9/14 開工、10/13 結束、一人全端、每天要發一篇 | 60 個 task、8 個 phase,和一張日期 × 里程碑總表 |
全程 docs/ 一個檔都沒動 —— 每一步都 git status 核過。它想回寫的內容另外寫成一份 spec-writeback.md,要不要動由我自己決定。
先講這 21 個怎麼來的。spec-kit 的 specify 預設最多只留 3 個 [NEEDS CLARIFICATION],其餘要它自己做合理推測 —— 工具的預設是替你猜。我反過來要求它一個都不准猜、全部標出來,結果不是 3 個,是 21 個。
下一步的 clarify 也有上限:它的 command 原文寫著「Maximum of 5 total questions across the whole session」—— 一個 session 最多問 5 題。所以我沒走它的問答,直接把 21 條的答案寫成一份檔餵進去,要它逐條清掉標記。
昨天客戶的 SPEC 被問了七題,今天我自己的規格被標了 21 處。分類:
| 類 | 條數 | 例子 |
|---|---|---|
| 範圍 | 4 | web 做哪些頁?iOS「骨架」有哪些畫面? |
| 資料模型 | 5 | 票種名額、活動名額、100 個座位三者什麼關係?hold 綁不綁票種? |
| 流程 | 5 | 改票價後,還沒確認的 hold 用哪個價?取消訂單座位釋不釋放? |
| 安全 | 5 | token 效期?refresh 重放偵測後撤多少?QR 裡放什麼? |
| 授權 | 1 | 「主辦」是建活動的那個 staff,還是任何 staff? |
| 細節 | 1 | 十分鐘「預設」可不可以覆寫? |
每一條讀了都覺得「這我當然知道」—— 然後發現規格裡真的沒寫。這跟 Day 7、8 是同一件事:規格先行擋的是範圍擴張,擋不了空白。空白要靠有人來問,而人不會問自己。
所以「規格不准擴張」要講精確:AI 不得自己填空;但它問了、我答了,空白就變成我的決定。 前者是擴張;後者是把原本的空白補成規格。
一口氣答了 21 條。答完它做了一件我沒料到的事:檢查我的答案之間合不合。C3 我說「一個 hold 綁座位加票種」,C9 說「早鳥折扣套在整筆小計上」—— 它指出如果一個 hold 可以混票種、各票種早鳥百分比不同,就沒有單一百分比可以套在小計上;它先採「一個 hold 一個票種」寫進假設,並標出來要我確認。另外兩處是答案跟原文打架:原文狀態機有 cancelled,今天說不做;原文只寫確認後金額不變,今天多說了「未確認的 hold 以新價計」。三處回頭看,都對。
plan 那步最值錢的是 constitution gate。五條硬規則它逐條檢查設計,結論是「無違反」—— 但它自己在四個地方標了 ⚠️:沒有違規,但值得人介入的風險或未決設計。它替我把還沒決定的地方攤了出來:
batch()。規則要「條件寫在 WHERE,看 changes」,但 D1 的 batch 只在拋錯時回滾 —— WHERE 沒命中不是語句失敗,changes = 0 不會觸發回滾。所以名額不足時要讓整批座位一起失敗,真正做事的是 CHECK (remaining >= 0),WHERE 只是不讓正常路徑撞到 CHECK。它列了三個候選、沒選 —— 因為那正是 Day 27 要自己定的規則。UPDATE … SET *_cents,所以 hold 不能先建一列 orders 再回填金額;訂單列與金額必須在確認那次一起 INSERT。表怎麼拆,留給明天。web/。check-money.sh 只掃 src/,web 端的金額顯示沒有裁判;它提議集中到一個檔、擴掃描範圍、補 self-test 探針。Swift 那邊沒有裁判,只能在 verified.md 記 ❌。src/lib/ 是偷加的目錄。CLAUDE.md 只列了三層,它要新增第四層放 JWT、HMAC、SQL,所以要求先補 CLAUDE.md —— 不補等於違反「不要主動加東西」。第一條讓我確定 Day 27 的題目是對的;第四條則說明 constitution 真的有在讀。
另外,它讀 repo 時還抓到兩個已經過時的地方:CLAUDE.md 規則 IV 寫「常數時間比對」、docs/spec.md 寫 crypto.subtle.verify,兩邊說法不一致;規則 V 註記「另一半還沒有自動檢查」,但 tests/schema.test.js 第 ⑥ 條從 9/10 起就會為它變紅。
還有一件它做對的事:repo 裡的 docs/EXPERIMENT-PROTOCOL.md 寫了四個「作者手寫、AI 不得先出版本」的實驗對象(schema、金額、時間、併發)。它讀到了,9 個 task 被標成 🖐(8 個實驗、1 個是我自己回寫規格),並註明「本 session 看過考題,不能扮演乾淨 session」。
這是今天的主線。它的 60 個 task 每一個有建議日期,文末一張總表;我的施工表是 8 月下旬排的。兩張表攤開來對:
| 里程碑 | 我排 | 它排 | 差 |
|---|---|---|---|
| JWT + 註冊登入 | 9/18–9/20 | 9/18–9/20 | 0 |
| 17 條 endpoint 全通(當時規劃 17 條) | 9/21 | 10/2 | +11 天 |
| schema 之外的三個實驗(金額/時間/併發) | 9/22–9/24,一天一個 | 9/24–9/30,作者版與 AI 版各占一天 | +4 天 |
| web 前端 | 9/25–9/28 | 10/4–10/6 | +9 |
| web 上線 | 9/29 | 10/10 | +11 |
| iOS 骨架 | 9/30–10/2 | 10/7–10/9 | +7 |
| 緩衝 | 無 | 10/11–10/13 | — |
差 11 天,不是它悲觀,是假設不同:我按整天排,實驗一天一個;它按「每天半天」排(我告訴它每天要發一篇文),而且堅持紅測試先行、每個實驗作者版與 AI 版分開兩天。這三個假設,回頭看都是它比較保守、也比較完整。
但它有一件事不知道,因為我沒告訴它:文章的發文日不等工程。Day 26 要在一個真的跑在線上的系統上等一個開賣時刻,Day 30 的額度表要有東西被打過 —— 那兩篇分別在 10/10 和 10/14 發,素材要在那之前存在。它的排法把 web 上線推到 10/10,到發文那天,素材還沒準備好。規劃工具最佳化的是你給它的目標;你沒給的,它不會替你守。
所以最後的決定:phase 順序用它的,日期用我的。兩張表誰排得準,Day 30 對帳。
排工是 9/14 做的,真正動手是 9/27。開工第一件事不是寫功能,而是讓一個什麼都不做的 URL 出現在網路上。
理由很實際:部署是整個專案裡最容易被拖到最後、然後在最後一天爆炸的事。第一天就把它做完,後面每一次改動都只是往一條已經通的路上加東西。
npx wrangler d1 execute signup --remote --file schema.sql # 遠端建表:8 張
npx wrangler deploy
curl -i https://event-signup.<你的子網域>.workers.dev/health
# HTTP/2 200
# x-server-now: 1790479798788
# {"ok":true,"server_now":1790479798788}
真的做下去,最先卡住我的不是程式碼,是權限:
wrangler login 預設要 28 項權限 —— DNS、Billing、AI、Queues 全包。這個專案只需要 Workers Scripts、D1、Pages,所以改用 --scopes 指定:
npx wrangler login --use-keyring --scopes account:read user:read workers_scripts:write d1:write pages:write
授權頁從 28 項變成 6 項。--use-keyring 讓 token 進鑰匙圈,不留明文檔。
offline_access 不能自己指定 —— 寫進 --scopes 會直接報 Invalid authentication scope。它是授權頁上那個必要的「Background Access」,wrangler 自己會加。
wrangler whoami 會警告缺 workers:write,但部署、cron、secret 都用不到它 —— 警告不等於需要。
seed 不上線。 本機的種子帳號密碼是 password123,放到公開網址上等於任何人都能用 staff 身分登入。線上的資料另外建。
三個免費方案我都用過。這次選型的判準只有一條:以這個專案 30 天的預估用量碰不到免費額度,而且不會因為閒置被暫停。額度還是有的 —— D1 免費方案每天 500 萬列讀、10 萬列寫,超過當日額度後,後續查詢會直接失敗,等 UTC 午夜重置。只是以這個專案的用量,差得很遠(Day 30 會算給你看)。
| 免費額度 | 閒置時暫停 | 這個專案的痛點 | |
|---|---|---|---|
| Supabase | 500 MB DB | 會 | 一週沒人用就要手動喚醒,寫連載時很致命 |
| Firebase | Spark 方案(每日 5 萬次文件讀取) | 不會 | Firestore 按文件讀寫次數計費,而活動列表、我的票券都是整批讀 |
| Cloudflare | Workers 10 萬請求/日 + D1 每日 500 萬列讀、10 萬列寫;單庫 500 MB | 不會 | ⭐ 採用 |
這一欄說的是會不會因為低活動被整個停掉,不是指冷啟動;免費額度三家都有。
但真正決定的不是這張表,是一個缺點:
D1 是 SQLite,沒有
SELECT ... FOR UPDATE。
這對一個要處理「開賣瞬間搶名額」的專案來說,聽起來像是選錯了。我知道,而我還是選了它 —— 因為那個限制正好是 Day 27 整篇的題目。一個沒有行鎖的資料庫,能不能靠其他機制守住不超賣,比直接用一個有行鎖的資料庫更值得做成實驗。
活動報名。限量名額、可以選位、保留十分鐘、逾時自動釋放、確認後出票。
不收錢。
對外它是報名系統,對內跑的是售票的規則 —— 限量、搶、狀態機、逾時。差別只在收不收錢,而規則的張力一點都不因為不收錢而減少。
開賽前寫好的那兩份檔案今天正式上場。docs/non-goals.md 現在就在 repo 的 docs/ 底下,而它比功能清單重要。
挑五條最會被 AI 越過的:
| # | 不做 | 為什麼 |
|---|---|---|
| 3 | 線上金流 | 只記帳不收錢,範圍與合規都爆炸 |
| 4 | 真實場館座位圖 | 只做 10×10 方格,不畫舞台 |
| 11 | 管理後台 | 用 wrangler d1 execute 直接下 SQL |
| 13 | 驗票掃碼入場 | 票券顯示 QR,不做掃描端 |
| 15 | UI 精緻度 | 系統元件不炸掉即可 |
完整 15 條在 repo 的 docs/non-goals.md:github.com/n913239/event-signup-lab/blob/main/docs/non-goals.md
部署這一步沒有測試可跑,所以裁判很原始:
curl -s https://<url>/health | grep '"ok":true'
會通就是通,不會通就是沒做完。第一天的驗收標準不需要更複雜。
這一篇留下的心法:
規格先行擋的是範圍擴張,擋不了空白 —— 空白要靠有人來問,而人不會問自己;它問了、我答了,那一條就不是擴張,是我的決定。規劃工具只替你守你餵給它的目標:我沒說「發文日不等工程」,它就把上線推到發文當天 —— 所以 phase 順序用它的,日期用我的。
明天:換一個乾淨的空目錄,讓另一個 AI 只拿一句需求設計 schema;跟 repo 裡那份讀過規格的版本,拿同一份 tests/schema.test.js 量。
specify init --here --integration claude;上面兩處預設值出自 v1.0.6 的 templates/commands/specify.md(LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total)與 clarify.md(Maximum of 5 total questions across the whole session);相關產出(constitution、spec、plan、openapi.yaml、tasks)在 repo 的 .specify/ 與 specs/001-event-signup-full/(腳手架 be5d262、五步產出 bca4d9a);spec-writeback.md 是 clarify 那一輪依我的要求另外寫的回寫指引,不是 spec-kit 的標準產出git status 確認 docs/ 未動);部署是 2026-09-27docs/non-goals.md 完整 15 條):github.com/n913239/event-signup-lab