庫(kù)里的工程基礎(chǔ)設(shè)施)
OpenSpec 使用教程把“需求文檔”變成工程基礎(chǔ)設(shè)施搞了這么多年軟件研發(fā)我一直被一個(gè)問(wèn)題困擾規(guī)格說(shuō)明總是和代碼脫節(jié)。需求散落在在線文檔、聊天記錄、會(huì)議紀(jì)要里開(kāi)發(fā)做起功能來(lái)全憑口口相傳等代碼寫完文檔早就過(guò)期了下一個(gè)接手的人只能對(duì)著代碼反推業(yè)務(wù)邏輯。前一陣子我接觸到了 OpenSpec它提出的思路很直接——把規(guī)格說(shuō)明本身當(dāng)成代碼倉(cāng)庫(kù)里的頭等公民用一套結(jié)構(gòu)化的規(guī)范文件替代亂七八糟的文檔碎片。這玩意兒目前社區(qū)討論熱度正在起來(lái)但完整的中文教程還挺少。這篇文章把我最近用 OpenSpec 梳理項(xiàng)目需求的完整過(guò)程復(fù)盤一遍包括核心概念拆解、目錄結(jié)構(gòu)設(shè)計(jì)、變更集機(jī)制、實(shí)際操作步驟和踩過(guò)的幾個(gè)坑適合正在苦惱需求文檔難維護(hù)、變更追蹤靠人肉記憶、評(píng)審全靠開(kāi)會(huì)口述的獨(dú)立開(kāi)發(fā)者和中小團(tuán)隊(duì)參考。1. 項(xiàng)目整體設(shè)計(jì)與思路拆解1.1 OpenSpec 到底解決了什么問(wèn)題先聊一下我看到的痛點(diǎn)。傳統(tǒng)的文檔驅(qū)動(dòng)開(kāi)發(fā)流程里一個(gè)功能從想法到落地通常要經(jīng)歷“需求文檔 → 技術(shù)設(shè)計(jì) → 編碼 → 聯(lián)調(diào) → 上線”這么幾個(gè)階段。問(wèn)題出在第二個(gè)環(huán)節(jié)到第三個(gè)環(huán)節(jié)之間技術(shù)設(shè)計(jì)往往寫得很粗開(kāi)發(fā)者拿到手以后憑經(jīng)驗(yàn)把設(shè)計(jì)翻譯成代碼等代碼寫完了這份設(shè)計(jì)文檔就成了歷史文物再也沒(méi)有人更新它。團(tuán)隊(duì)成員換幾輪以后系統(tǒng)邏輯就變成了大爺們腦子里的傳說(shuō)。OpenSpec 的思路是把規(guī)格說(shuō)明文件直接放進(jìn) Git 倉(cāng)庫(kù)用一套結(jié)構(gòu)化的 markdown 文件來(lái)描述業(yè)務(wù)規(guī)則、使用場(chǎng)景、驗(yàn)收標(biāo)準(zhǔn)和實(shí)現(xiàn)細(xì)節(jié)然后通過(guò)命令行工具把這些說(shuō)明文件變成可執(zhí)行的工作流。你提交代碼的時(shí)候順便提交規(guī)格文件的變更代碼評(píng)審的時(shí)候規(guī)格文件也要一起評(píng)審CI 流程里可以校驗(yàn)規(guī)格文件格式和覆蓋情況。它的核心價(jià)值我用一句話總結(jié)讓需求定義和代碼實(shí)現(xiàn)同步演進(jìn)讓“文檔過(guò)期”這件事從物理上變得不可能。1.2 設(shè)計(jì)理念當(dāng)需求描述變成代碼評(píng)審的對(duì)象我最欣賞 OpenSpec 的一點(diǎn)是它重新定義了“需求描述”在項(xiàng)目里的地位。傳統(tǒng)方法里需求描述放在需求管理工具里和代碼倉(cāng)庫(kù)是兩套系統(tǒng)中間靠人肉同步。OpenSpec 把規(guī)格說(shuō)明做成了代碼倉(cāng)庫(kù)的一部分這意味著它天然具備版本管理、變更歷史、分支隔離、評(píng)審流程這些工程能力。舉個(gè)生活化的類比以前的需求管理方式就像家里貼滿便利貼每張上寫著一句“記得買醬油”貼得多了誰(shuí)貼的、什么時(shí)候貼的、哪條已經(jīng)過(guò)期了全亂了。OpenSpec 的做法是把便利貼整理成一本家庭賬冊(cè)每次更新都要記錄改動(dòng)人、改動(dòng)時(shí)間和改動(dòng)原因買東西之前先查賬買完回來(lái)銷賬。這個(gè)設(shè)計(jì)的好處體現(xiàn)在三點(diǎn)變更可追溯任何一個(gè)規(guī)格文件的改動(dòng)都有 commit 記錄什么時(shí)候誰(shuí)改了什么規(guī)則清清楚楚。評(píng)審前置代碼評(píng)審時(shí)可以同時(shí)審查規(guī)格變更避免“代碼實(shí)現(xiàn)了但是需求定義早就變了”的尷尬局面。自動(dòng)化落地CLI 工具可以根據(jù)規(guī)格文件的組織結(jié)構(gòu)自動(dòng)生成工作流比如列出所有待評(píng)審的 proposal、檢查某個(gè)需求是否被覆蓋。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 規(guī)格目錄結(jié)構(gòu)一切從 opspec 文件夾開(kāi)始先用一個(gè)真實(shí)項(xiàng)目的目錄結(jié)構(gòu)來(lái)說(shuō)明 OpenSpec 是怎么組織信息的。在項(xiàng)目根目錄下初始化 OpenSpec 以后會(huì)生成一個(gè)opspec/文件夾內(nèi)部結(jié)構(gòu)類似這樣opspec/ ├── project.md ├── specs/ │ ├── cart/ │ │ ├── spec.md │ │ └── scenarios/ │ │ ├── add-to-cart/ │ │ │ └── adds-item-to-cart.md │ │ └── remove-from-cart/ │ │ └── removes-item-from-cart.md │ ├── checkout/ │ │ ├── spec.md │ │ └── scenarios/ │ │ └── process-payment/ │ │ └── processes-payment-successfully.md │ └── user/ │ ├── spec.md │ └── scenarios/ │ └── user-registration/ │ └── registers-new-user.md ├── changesets/ │ └── 20250212_add-user-registration/ │ ├── proposal.md │ └── user/ │ └── specification.md └── worktrees/核心目錄是specs/和changesets/。specs/下按業(yè)務(wù)域如cart、checkout、user拆分每個(gè)域有一份spec.md描述整體規(guī)則一個(gè)scenarios/目錄描述具體場(chǎng)景。changesets/則是待處理的變更集配合 Git worktree 使用形成一套提案-評(píng)審-合并流程。這里的組織方式和 Clean Architecture 的分層思路很像業(yè)務(wù)域是最高維度場(chǎng)景是業(yè)務(wù)域下面的能力點(diǎn)每個(gè)能力點(diǎn)對(duì)應(yīng)一個(gè)輸入輸出的完整描述。寫起來(lái)的時(shí)候不用想架構(gòu)只需要想清楚“我這個(gè)系統(tǒng)有哪些業(yè)務(wù)域每個(gè)業(yè)務(wù)域下有什么場(chǎng)景”剩下的結(jié)構(gòu) OpenSpec 自動(dòng)幫你鋪好了。2.2 規(guī)格文件的書寫規(guī)范與邏輯層級(jí)規(guī)格文件的書寫有一套約定俗成的結(jié)構(gòu)。spec.md是整個(gè)業(yè)務(wù)域的入口文件里面要聲明該域的需求和場(chǎng)景清單。以購(gòu)物車域?yàn)槔? Cart Domain Specification ## Requirements - 用戶可以添加商品到購(gòu)物車 - 用戶可以修改購(gòu)物車中商品的數(shù)量 - 用戶可以刪除購(gòu)物車中的商品 - 購(gòu)物車中的商品數(shù)量不能超過(guò) 99 ## Scenarios - add-to-cart: 添加商品到購(gòu)物車 - remove-from-cart: 從購(gòu)物車移除商品每個(gè)場(chǎng)景文件則定義一條具體的規(guī)則描述用 Given-When-Then 的形式展開(kāi)# Adds an item to the cart ## Requirements - 添加商品時(shí)如果商品已存在數(shù)量自動(dòng)累加 - 添加商品后返回購(gòu)物車的最新總金額 ## Scenario: 基礎(chǔ)添加流程 - **When** 用戶發(fā)送添加商品請(qǐng)求商品ID: 88數(shù)量: 1 - **Then** 購(gòu)物車創(chuàng)建一條明細(xì)記錄商品數(shù)量為 1 - **And** 返回購(gòu)物車總金額為 88 元 ## Scenario: 重復(fù)添加商品 - **When** 用戶再次添加商品ID: 88數(shù)量: 2 - **Then** 購(gòu)物車原有數(shù)量從 1 累加為 3 - **And** 返回購(gòu)物車總金額為 264 元我一開(kāi)始對(duì)這種寫法有點(diǎn)抵觸覺(jué)得它像測(cè)試用例又不像測(cè)試用例。用了一段時(shí)間以后發(fā)現(xiàn)這種格式的價(jià)值在于強(qiáng)制你窮舉場(chǎng)景邊界。當(dāng)你必須把“When... Then...”寫清楚的時(shí)候那些含糊其辭的“用戶應(yīng)該能方便地管理購(gòu)物車”之類的話就寫不出去了。如果你發(fā)現(xiàn)自己寫不清 When 和 Then大概率是你自己都沒(méi)搞明白需求。2.3 變更集 Worktree 機(jī)制讓需求評(píng)審像代碼評(píng)審一樣嚴(yán)謹(jǐn)changesets/是 OpenSpec 最值得稱道的機(jī)制。它借鑒了 Git worktree 的場(chǎng)景隔離思路把每個(gè)待開(kāi)發(fā)的需求提案放在獨(dú)立的目錄里目錄名是提案唯一標(biāo)識(shí)里面包含一份proposal.md和按域組織的規(guī)格變更文件。工作流程是這樣的基于主倉(cāng)庫(kù)切一個(gè) worktree 分支。在opspec/changesets/下創(chuàng)建變更集目錄寫清楚這個(gè)提案改了什么、為什么改。修改對(duì)應(yīng)域的規(guī)格文件把新需求、新場(chǎng)景寫進(jìn)去。提交代碼發(fā)起 Pull Request。評(píng)審?fù)ㄟ^(guò)后合并變更變更集里的規(guī)格文件會(huì)同步到主specs/目錄。這個(gè)流程的好處是需求變更不再是一句輕飄飄的“改一下規(guī)則”而是一條有完整上下文的工程記錄。Reviewer 看到的不只是“這里加了個(gè) if 判斷”而是“新增了一條規(guī)則購(gòu)物車數(shù)量上限 99對(duì)應(yīng)場(chǎng)景文件是 X測(cè)試用例是 Y”。3. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 環(huán)境準(zhǔn)備與初始化 OpenSpecOpenSpec 目前提供 Node.js 版本的和 Rust 版本的 CLI建議直接用 Node.js 的 npm 包安裝最穩(wěn)npm install -g openspec/cli或者如果你在用 Rust 的工具鏈cargo install openspec安裝完以后驗(yàn)證一下版本openspec --version在已有項(xiàng)目里初始化openspec init初始化會(huì)在項(xiàng)目根目錄生成opspec/目錄和基礎(chǔ)的模板文件。如果你用的是已經(jīng)存在的倉(cāng)庫(kù)建議先提交一版代碼再初始化不然opspec/目錄的變化會(huì)和項(xiàng)目現(xiàn)有改動(dòng)混在一起看起來(lái)不干凈。3.2 創(chuàng)建第一個(gè)變更集并編寫提案假設(shè)我們想給電商系統(tǒng)加一個(gè)新功能用戶注冊(cè)成功后自動(dòng)發(fā)送歡迎郵件。第一步創(chuàng)建變更集openspec changeset add-user-registration-email命令會(huì)在opspec/changesets/下生成一個(gè)以當(dāng)前日期和描述命名的目錄。進(jìn)去以后編輯proposal.md# Add User Registration Email ## Why 用戶注冊(cè)完成后缺少郵件觸達(dá)導(dǎo)致新用戶無(wú)法及時(shí)了解平臺(tái)權(quán)益完整體驗(yàn)度低。 ## What Changes - 用戶注冊(cè)成功事件觸發(fā)后系統(tǒng)自動(dòng)發(fā)送歡迎郵件 - 郵件內(nèi)容包含用戶名、注冊(cè)時(shí)間和新手入門指引鏈接 - 郵件發(fā)送失敗不阻斷注冊(cè)流程失敗信息進(jìn)入日志系統(tǒng) ## Impact - User 域新增 event handler依賴郵件服務(wù) - 新增一個(gè)異步任務(wù)處理郵件發(fā)送邏輯這份 proposal 的意義在于它讓所有相關(guān)方在寫代碼前對(duì)齊三件事改什么、為什么改、影響范圍有多大。我曾經(jīng)見(jiàn)過(guò)太多需求階段吵得不可開(kāi)交、代碼評(píng)審時(shí)候反而沒(méi)有分歧的項(xiàng)目原因就是前期根本沒(méi)有把“What”和“Impact”寫清楚。3.3 修改規(guī)格文件并跟蹤需求變更提案創(chuàng)建完以后在同一個(gè)變更集目錄下按照域結(jié)構(gòu)創(chuàng)建新的規(guī)格文件。OpenSpec 提供了一個(gè)交互命令輔助完成openspec butterfly這個(gè)命令會(huì)讀取變更集目錄下的所有 markdown自動(dòng)提取信息并幫助你把當(dāng)前域的需求和場(chǎng)景整合進(jìn)臨時(shí) spec 文件。如果你更習(xí)慣手動(dòng)控制也可以自己手寫我一般用 butterfly 生成初稿后再手動(dòng)調(diào)整效率更高。在變更集里user/域的規(guī)格文件增補(bǔ)內(nèi)容大概長(zhǎng)這樣# User Domain Specification ## Requirements - [x] 用戶可以注冊(cè)新賬號(hào) - [ ] 注冊(cè)完成后發(fā)送歡迎郵件 - [ ] 歡迎郵件發(fā)送失敗可以重試 3 次 ## Scenarios - registration-email-notification: 注冊(cè)成功后發(fā)送郵件通知注意這里新增了registration-email-notification場(chǎng)景那對(duì)應(yīng)的場(chǎng)景文件也要補(bǔ)全# Sends registration email notification ## Requirements - 注冊(cè)成功后 1 分鐘內(nèi)發(fā)送歡迎郵件 - 郵件內(nèi)容包含用戶昵稱 - 郵件服務(wù)不可用時(shí)發(fā)送任務(wù)進(jìn)入重試隊(duì)列 ## Scenario: 正常發(fā)送 - **When** 用戶提交注冊(cè)表單并成功創(chuàng)建賬號(hào) - **Then** 系統(tǒng)在 60 秒內(nèi)觸發(fā)發(fā)送任務(wù) - **And** 郵件內(nèi)容包含用戶昵稱和注冊(cè)時(shí)間 ## Scenario: 郵件服務(wù)不可用 - **When** 郵件服務(wù)返回超時(shí)錯(cuò)誤 - **Then** 注冊(cè)流程正常結(jié)束不拋異常 - **And** 發(fā)送任務(wù)進(jìn)入重試隊(duì)列最多重試 3 次3.4 驗(yàn)證規(guī)格文件格式并用工作流推進(jìn)合并寫完了變更集里的規(guī)格文件怎么確認(rèn)格式?jīng)]問(wèn)題OpenSpec 提供了配套命令其中之一會(huì)在后臺(tái)啟動(dòng)一個(gè)本地儀表盤讓你在網(wǎng)頁(yè)上可視化查看規(guī)格文件的覆蓋情況和變更集狀態(tài)openspec server跑起來(lái)以后瀏覽器打開(kāi)http://localhost:3000就能看到項(xiàng)目的規(guī)格結(jié)構(gòu)。這個(gè)頁(yè)面特別適合團(tuán)隊(duì)評(píng)審?fù)镀琳故颈却蠹腋勺?IDE 里的 markdown 直觀得多。另外一個(gè)核心命令是openspec check它會(huì)遍歷整個(gè)opspec/目錄檢查所有 spec 文件的格式正確性比如每個(gè) scenario 是否都有對(duì)應(yīng)的 When/Then、Requirements 里有沒(méi)有引用不存在的場(chǎng)景、文件命名是否符合 kebab-case 規(guī)范。我在集成 CI 的時(shí)候就把openspec check加進(jìn)了 pipeline規(guī)格文件格式不過(guò)關(guān)直接不讓合并省去了大量人工 review 格式的精力。檢查通過(guò)以后用常規(guī) Git 流程提交代碼git add opspec/ git commit -m feat(spec): add user registration email proposal git push origin feature/user-registration-email合并完代碼以后OpenSpec 會(huì)有一個(gè)步驟把變更集里的規(guī)格變更合并回主specs/目錄完成規(guī)格的“落庫(kù)”。這一步如果用的是 worktree 分支用openspec merge它會(huì)自動(dòng)解析最近合并的變更集把提案中涉及的域合并進(jìn)specs/對(duì)應(yīng)位置。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 變更集目錄命名混亂導(dǎo)致合并沖突OpenSpec 默認(rèn)用“日期 描述”作為變更集目錄名比如20250212_add-user-registration-email。如果你一天提交多個(gè)變更集日期重復(fù)的概率很高如果團(tuán)隊(duì)里幾個(gè)人同時(shí)創(chuàng)建變更集目錄名沖突更是家常便飯。我一個(gè)比較深刻的教訓(xùn)是某次功能拆到一半同事和我同時(shí)跑了openspec changeset add-payment-webhook結(jié)果兩個(gè)人的目錄名一模一樣合代碼時(shí)先合并的人把另一個(gè)人的文件覆蓋了找問(wèn)題找了半小時(shí)。建議做法在變更集目錄名里加上唯一的短標(biāo)識(shí)比如需求編號(hào)或者 jira ticket 號(hào)20250212_ops-142_add-user-registration-email。同時(shí)約定一個(gè)規(guī)范變更集目錄創(chuàng)建后proposal.md 第一個(gè) commit 必須在 5 分鐘內(nèi)完成避免目錄一直處于“半成品”狀態(tài)。4.2 規(guī)格文件更新了但代碼沒(méi)跟上這是整個(gè)流程里最容易出現(xiàn)的“假閉環(huán)”。提案合并進(jìn)主分支后規(guī)格文件永遠(yuǎn)領(lǐng)先于代碼實(shí)現(xiàn)。OpenSpec 本身有一定的約束機(jī)制可以通過(guò)類似openspec status之類的功能查看變更集在代碼中的落地狀態(tài)但它本質(zhì)上是文檔工具管不住開(kāi)發(fā)者不寫代碼。我的實(shí)操心得是把規(guī)格文件的合并當(dāng)成開(kāi)發(fā)任務(wù)的開(kāi)始而不是結(jié)束。創(chuàng)建變更集只是完成了“需求定義”后續(xù)要在同一條分支上繼續(xù)寫實(shí)現(xiàn)代碼然后在聯(lián)調(diào)完成后回到方案階段用真實(shí)驗(yàn)收結(jié)果對(duì)照規(guī)格文件里的 Scenario 過(guò)一遍。4.3 場(chǎng)景文件拆太細(xì)導(dǎo)致維護(hù)成本上升用 OpenSpec 一段時(shí)間后很容易犯一個(gè)毛病把所有能想到的邊界情況都拆成獨(dú)立場(chǎng)景文件。界面上的一個(gè)按鈕拆出“正常點(diǎn)擊”“快速雙擊”“點(diǎn)擊后即關(guān)閉頁(yè)面”“斷網(wǎng)點(diǎn)擊”四個(gè)場(chǎng)景。結(jié)果規(guī)格數(shù)量爆發(fā)式增長(zhǎng)寫文檔的時(shí)間比寫代碼還多。這個(gè)問(wèn)題的根源是把 OpenSpec 當(dāng)成了測(cè)試用例管理工具而它的定位是需求規(guī)格定義工具。測(cè)試用例該歸測(cè)試框架管OpenSpec 只需要描述清楚哪條業(yè)務(wù)規(guī)則存在、它的輸入輸出預(yù)期是什么。細(xì)節(jié)分支留給測(cè)試用例去窮舉。我個(gè)人的經(jīng)驗(yàn)法則一個(gè)業(yè)務(wù)域下 5-10 個(gè)場(chǎng)景是健康的超過(guò) 15 個(gè)就該回頭審視是不是拆粒度過(guò)細(xì)了。4.4 變更集被 merge 了但主 specs 目錄沒(méi)更新我自己踩過(guò)的坑之一提案分支合并到主分支后opspec/specs/目錄下的規(guī)格文件沒(méi)有同步更新導(dǎo)致主分支的規(guī)格落后于實(shí)際代碼。后面排查發(fā)現(xiàn)是中途手動(dòng) merge 代碼時(shí)繞過(guò)了openspec merge這一步把變更集的推進(jìn)動(dòng)作漏掉了。避坑技巧在 pull request 模板里把“是否已執(zhí)行 openspec merge”加進(jìn)勾選清單或者配置一個(gè)手動(dòng) CI job專門檢查當(dāng)前 HEAD 下是否有已合并但未同步的 changeset。如果你合并操作比較頻繁也可以在合并腳本里串上openspec merge命令做成一條龍操作。我個(gè)人在實(shí)際操作中的體會(huì)是OpenSpec 真正改變的不是文件組織方式而是團(tuán)隊(duì)對(duì)“需求定義”這件事的重視程度。以前需求評(píng)審大家靠嘴說(shuō)現(xiàn)在所有討論都沉淀在規(guī)格文件里新的開(kāi)發(fā)者翻開(kāi)opspec/specs/目錄就能了解全貌這種體驗(yàn)是用多少并發(fā)需求文檔都換不來(lái)的。最后再分享一個(gè)小技巧如果你剛上手玩這個(gè)項(xiàng)目別一開(kāi)始就逼團(tuán)隊(duì)全量遷移先挑一個(gè)小業(yè)務(wù)域跑通提案-評(píng)審-合并流程等大家習(xí)慣了這種工作方式再逐步推廣阻力會(huì)小很多。