Compare commits
13
Commits
a9ef078ef6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d360ad266 | ||
|
|
2b61c4e235 | ||
|
|
fdd5696c7b | ||
|
|
6080cc99da | ||
|
|
43e91ceb70 | ||
|
|
c56f3271b5 | ||
|
|
52b5f08e7e | ||
|
|
957dd8795b | ||
|
|
37ecd93c6b | ||
|
|
0254c3abe1 | ||
|
|
ddc72a404e | ||
|
|
76cdedc980 | ||
|
|
744eee04ba |
@@ -28,6 +28,8 @@
|
||||
## 慣例
|
||||
|
||||
- **零外部套件**:`package.json` 不得出現 `dependencies` 或 `devDependencies`。測試用 Node 內建 `node:test` + `node:assert`。
|
||||
- **委派標記雙向一致**:流程正本的 `〔可委派〕` 集合必須與 `references/delegation.md` 相同;任何變更同步更新資產測試與 ADR。
|
||||
- **文字編碼**:流程正本、規則正本、README、AGENTS.md 與 ADR 一律以 UTF-8 儲存,面向使用者的文字維持繁體中文。
|
||||
- **契約以議題為正本**:腳本的 flag 介面、JSON 輸出形狀、前置檢查與路徑定位規則,正本在[議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1),實作時以該處為準;本檔不複寫,以免兩邊走鐘。
|
||||
- **測試**:`npm test`(等同 `node --test`)。測試產生的暫存一律寫到 `.tmp/`,該目錄已被 git 忽略,也不會被測試探索掃到。
|
||||
- **不改目標專案**:本 plugin 只讀目標專案的程式碼,不寫入目標專案的 `CLAUDE.md` 或任何設定檔。
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
- **副作用集中**:所有對 Gitea 與 git 的呼叫下沉到 `scripts/` 的零相依 Node 腳本,統一 JSON 輸入輸出。
|
||||
- **產出有固定形狀**:議題、PR、報表一律套 `templates/` 的模板。
|
||||
|
||||
> 六個流程正本都到齊了,`tea-sdlc install` 會把它們一次佈署到偵測到的平台。
|
||||
> 完整需求見[議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1),進度見其底下的工作包。
|
||||
> 六個流程正本都到齊了;`tea-sdlc install` 會把它們一次佈署到偵測到的平台。委派標記以
|
||||
> `references/delegation.md` 為對照正本,資產測試會檢查雙向一致;流程與文件均以 UTF-8
|
||||
> 儲存並維持繁體中文。進度見
|
||||
> [議題 #1](https://gitea.jsc.idv.tw/plugins/tea-sdlc/issues/1) 底下的工作包。
|
||||
|
||||
---
|
||||
|
||||
@@ -41,6 +43,7 @@ tea-sdlc/
|
||||
├── plugin.json # Antigravity 的 plugin manifest
|
||||
├── package.json # npm 打包與測試入口,無任何相依套件
|
||||
├── AGENTS.md # 給 AI 助理的模組邊界與慣例
|
||||
├── docs/adr/ # 已接受的架構決策紀錄
|
||||
└── README.md
|
||||
```
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ status: accepted
|
||||
# 以能力描述而非工具名表達委派
|
||||
|
||||
流程正本在只在意結果的步驟上標記 `〔可委派〕`,並以**能力描述**說明怎麼委派——「你的環境若能把工作交給子代理,就交出去,只把結果帶回來;不能就自己做」——而不指名任何平台的子代理工具。子代理是平台專屬能力(Claude Code 與 Codex 有,Copilot/Kiro/OpenCode 不一定),而流程正本必須保持平台中立:這是 #4 已交付並打勾的驗收標準,也是 `AGENTS.md` 的模組邊界之一。能力描述對不支援的平台是自然降級,同一份正本兩邊都讀得通,不需要維護兩份。
|
||||
目前的標記集合是:`sdlc-plan` 列出九段落依據與缺漏、`sdlc-analyze` 對四份清單列出疑點與算出截止日,以及 `sdlc-feat` 把議題標題翻成英文與計算分批提交方案。分批提交的實際執行仍由主流程處理;完整對照以 `references/delegation.md` 為準。
|
||||
|
||||
## Considered Options
|
||||
|
||||
|
||||
+36
-5
@@ -9,6 +9,8 @@ description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可
|
||||
|
||||
## 第一段:可行性分析
|
||||
|
||||
### 1. 對四份清單列出疑點〔可委派〕
|
||||
|
||||
依序讀取並逐條對照:
|
||||
|
||||
1. `references/feasibility-architecture.md`
|
||||
@@ -16,19 +18,47 @@ description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可
|
||||
3. `references/feasibility-data.md`
|
||||
4. `references/feasibility-schedule.md`
|
||||
|
||||
能從程式碼查證的事項自行查證;只有需要使用者決策的事項才提問。架構、邏輯、資料、時程四類依序完成,每次只問一題,每題提供建議與手動輸入。最後輸出共識摘要、變更假設、未決事項與人天估算;摘要只印終端,不寫入 Gitea。
|
||||
只產出可核對的疑點清單,不替使用者做決策;你的環境若能把工作交給子代理,就交出去,只把清單帶回來;不能就自己做。
|
||||
|
||||
使用者確認共識摘要後,**先列出要交付或驗收的項目,逐項向使用者確認**。使用者未確認或拒絕時,立即停止,不建立工作包、不排程、不寫入後續資料。
|
||||
### 2. 逐題確認可行性共識
|
||||
|
||||
能從程式碼查證的事項自行查證;只有需要使用者決策的事項才提問。架構、邏輯、資料、時程四類依序完成,每次只問一題,每題提供建議與理由,以及手動輸入的方式。最後輸出共識摘要、變更假設、未決事項與人天估算;摘要只印終端,不寫入 Gitea。
|
||||
|
||||
### 交付文件判斷
|
||||
|
||||
使用者確認共識摘要後,讀取 `references/delivery-types.md`,列出這次需求需要交付或驗收的文件,並依序逐一詢問:
|
||||
|
||||
1. **需求描述概要**
|
||||
2. **WBS(工作分解結構)**
|
||||
3. **流程圖**
|
||||
4. **甘特圖**
|
||||
5. **PERT 圖**
|
||||
6. **關鍵路徑圖**
|
||||
7. **API 契約文件**
|
||||
|
||||
每一種文件都要先展示必要內容骨架,再給出是否需要的建議與理由,最後讓使用者確認、拒絕或手動調整;不得把七種文件合併成一次確認。確認結果要保留文件順序、必要內容、產出位置與 ELI5 變體規則。API 契約文件只能交付預覽或使用者確認的位置,禁止寫入目標專案 repo。
|
||||
|
||||
使用者未確認或拒絕任何一項時,立即停止,不建立工作包、不排程、不建立相依、不寫入 Milestone、看板或其他後續資料。
|
||||
|
||||
### PERT 三點估算
|
||||
|
||||
對每一個需要排程的工作包,分別逐題詢問樂觀時間(O)、最可能時間(M)、悲觀時間(P);每題都提供建議、理由與手動輸入方式。三個值都確認後才納入排程資料,不得用單一人天估算代替,也不得在確認前建立工作包或排程。
|
||||
|
||||
## 第二段:產生工作包
|
||||
|
||||
確認交付/驗收項目後,依共識切出可獨立完成的工作包,套用 `templates/work-package-issue.md`。每顆工作包的待辦與驗收都要可逐項勾選;對應已確認交付項目的待辦置於第一項。工作包整體也依交付優先排序,但不得違反先決關係。
|
||||
確認交付/驗收項目與所有必要的 PERT 三點估算後,依共識切出可獨立完成的工作包,套用 `templates/work-package-issue.md`。每顆工作包的待辦與驗收都要可逐項勾選;對應已確認交付項目的待辦置於第一項。
|
||||
|
||||
工作包排序先放已確認的交付文件工作包,再放純程式碼工作包;排序只在不違反先決關係時生效,若交付文件工作包依賴其他工作包,仍以拓撲順序為準。每顆工作包只做一種交付,並在描述中保留文件類型、必要內容、產出位置與驗收方式。
|
||||
|
||||
每顆工作包先以 `scripts/issue-create.js --dry-run` 檢查,再移除旗標實跑。只使用既有標籤。建立後回報編號、標題與網址。
|
||||
重跑同一需求時,先以既有議題與標題查重;已存在的工作包只沿用其編號與相依,不建立重複工作包。
|
||||
|
||||
## 第三段:排程
|
||||
|
||||
以 `startDate`、`days` 與 `depends` 組成計畫檔,執行 `scripts/schedule.js` 計算截止日;依序用 `issue-link.js`、`issue-update.js` 與 `project-add.js` 補上既有相依、Milestone、看板、截止日與人天估算。各腳本先 dry-run,再實跑。日期與人天是排程資料,不是耗時統計。
|
||||
### 3. 算出截止日〔可委派〕
|
||||
|
||||
所有工作包建立且相依關係確認後,只依已確認的 `startDate`、`days` 與 `depends` 呼叫 `scripts/schedule.js` 計算截止日;這一步只回傳可核對的日期與相依結果,你的環境若能把工作交給子代理,就交出去,只把結果帶回來;不能就自己做。寫入議題、Milestone、看板與其他後續資料不委派。
|
||||
計算完成後,依序用 `issue-link.js`、`issue-update.js` 與 `project-add.js` 補上既有相依、Milestone、看板、截止日與人天估算。各腳本先 dry-run,再實跑。日期與人天是排程資料,不是耗時統計。
|
||||
|
||||
## 邊界
|
||||
|
||||
@@ -36,4 +66,5 @@ description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可
|
||||
- 不建立 Milestone 或專案看板。
|
||||
- 不修改使用者專案檔案。
|
||||
- 不關閉或刪除既有議題。
|
||||
- 共識摘要與交付確認前不建立任何工作包。
|
||||
- 不把 API 契約文件寫入目標專案 repo。
|
||||
- 共識摘要、交付文件確認與 PERT 三點估算完成前,不建立任何工作包、不排程、不寫入後續資料。
|
||||
|
||||
+29
-2
@@ -3,14 +3,41 @@ description: 僅由 /sdlc-feat 指令叫用。領取工作包、開分支、逐
|
||||
|
||||
# sdlc-feat
|
||||
|
||||
輸入一個工作包議題編號。若抽取結果有 `未處理留言數`,直接走 `/sdlc-sync` 的流程,完成後自動接回這裡;不要要求使用者重打指令。接回前重新抽取一次拿到更新後的描述。
|
||||
輸入一個工作包議題編號。先用 `scripts/wp-extract.js` 抽取;若抽取結果有 `未處理留言數`,直接走 `/sdlc-sync` 的流程,完成後自動接回這裡;不要要求使用者重打指令。接回前重新抽取一次拿到更新後的描述。
|
||||
|
||||
依工作包 `repos` 逐一準備 worktree;不在主工作區切換分支。逐項完成待辦並立即勾選對應驗收。交付優先項目位於待辦第一項時先完成;仍須遵守工作包的依賴與範圍邊界。
|
||||
|
||||
完成後依既有 commit 分類規則提交,先用 `scripts/pr-create.js --dry-run` 檢查,再實跑開 PR。回報工作包、分支、worktree、完成待辦、commit 與 PR。
|
||||
## 交付文件待辦分流
|
||||
|
||||
先把每一項待辦判定為程式碼待辦或交付文件待辦;兩者不可共用無差別的實作流程。交付文件待辦是需求描述概要、WBS、流程圖、甘特圖、PERT 圖、關鍵路徑圖或 API 契約文件等文件產出,判定依 `references/delivery-types.md` 的內容與產出位置。
|
||||
|
||||
### 交付前逐一確認
|
||||
|
||||
每一份交付文件都要分開處理。產出前先列出該類型的必要內容骨架、已知來源、未決事項、建議與理由,再一次只問一題確認內容是否齊全;未確認的文件不可標成已交付。能從需求、工作包、相依或排程重新推導的內容,保留來源與推導規則,不自行編造決策。
|
||||
|
||||
### 預覽能力分流
|
||||
|
||||
以能力描述檢查目前環境是否能把該文件即時發佈成可開啟、可分享的預覽頁,並說明判定依據;不要靠固定清單或猜測環境。確認具備能力後直接產出預覽,回報實際位置與內容摘要。無法證明具備能力、預覽不可開啟或不可分享時,停止產出並一次只問一題,由 agent 依文件、來源與限制提出建議及理由,同時保留手動輸入,不替使用者決定交付方式。
|
||||
|
||||
API 契約文件只能交付在預覽位置或使用者確認的位置;不得寫入目標專案 repo,也不得藉由缺少預覽能力而改寫目標專案的設定檔或文件。預覽失效時回報實際限制,不捏造網址。
|
||||
|
||||
### ELI5 變體
|
||||
|
||||
使用者要求 ELI5 時,仍保留原文件的範圍、順序、相依、例外與驗收意義;把術語換成日常說法並補必要的短解釋,不刪除技術限制。圖表要重新繪製成容易閱讀的圖片式視覺,不把 Mermaid 原碼當成交付物,也不只用一個看似精確的日期隱藏 O/M/P 不確定性。
|
||||
### 把議題標題翻成英文〔可委派〕
|
||||
|
||||
把工作包議題標題轉成不超過 40 字元的英文 kebab slug,保留原意且不捏造新範圍;這一步只產出可驗證的 slug,你的環境若能把工作交給子代理,就交出去,只把 slug 帶回來;不能就自己做。若有兩個同樣合理的翻法,交回候選與差異,由主流程詢問使用者。
|
||||
|
||||
## 實作與交付
|
||||
### 分批提交方案〔可委派〕
|
||||
|
||||
依檔案類型與變更性質計算 commit 分類、順序與每批檔案;只回傳可核對的提交方案。實際執行 `scripts/commit-split.js`、處理失敗與確認 git 歷史不委派,由主流程自己完成。
|
||||
|
||||
完成程式碼待辦後照既有測試與驗證慣例;文件待辦則依上述分流交付。完成後依既有 commit 分類規則提交,先用 `scripts/pr-create.js --dry-run` 檢查,再實跑開 PR。回報工作包、分支、worktree、完成待辦、commit 與 PR。
|
||||
|
||||
## 邊界
|
||||
|
||||
- 不修改工作包範圍外的檔案。
|
||||
- 不切換主工作區分支。
|
||||
- 不產生固定選項清單,不把平台工具名或呼叫語法寫入流程正本。
|
||||
- 不操作碼錶、不補登工時、不產生耗時統計。
|
||||
|
||||
+29
-5
@@ -7,11 +7,35 @@ description: 僅由 /sdlc-plan 指令叫用。把一段口語需求轉成結構
|
||||
|
||||
## 步驟
|
||||
|
||||
1. 讀齊輸入,列出九個段落中已有依據與缺漏。
|
||||
2. 一次問一題補齊缺漏;未獲回答的內容放入「未決事項」,不得自行編造。
|
||||
3. 套用 `templates/requirement-issue.md`,填入總覽、背景、目標、非目標、領域名詞表、文件、驗收標準、影響範圍與未決事項;全程使用繁體中文。
|
||||
4. 用 `scripts/labels-list.js` 取得既有標籤,只能選既有標籤。
|
||||
5. 寫入前先執行:
|
||||
開始前讀取 `references/requirements-discovery.md`。它是需求內容品質與建立前完整性閘門的規則正本;本流程只補充操作順序與 Gitea 交付步驟。
|
||||
|
||||
### 1. 列出九段落依據與缺漏〔可委派〕
|
||||
|
||||
讀齊輸入,列出總覽、背景、目標、非目標、領域名詞表、文件、驗收標準、影響範圍與未決事項中已有依據與缺漏。對每一段不只檢查是否非空,還要依需求補全規則檢查實質內容。這一步只產出可核對的清單;你的環境若能把工作交給子代理,就交出去,只把清單帶回來;不能就自己做。
|
||||
|
||||
### 2. 一次問一題補齊缺漏
|
||||
|
||||
依需求補全規則挑出下一個最高價值的需求缺口。每次只問一題,並同時列出目前理解、缺少內容、為什麼需要,以及建議選項或短範例;明確允許使用者自由改寫答案。
|
||||
|
||||
能從輸入、既有議題或可取得的 repo 內容查證的事項先自行查證;只有需要需求擁有者決策的事項才提問。不得把推測或實作方案寫成已確認需求。
|
||||
|
||||
每次收到回答後更新工作稿,重新檢查角色、情境、行為、可觀察結果,以及主要成功情境與適用的失敗/邊界情境。未回答、「不知道」或「尚未決定」仍是缺口,繼續一次問一題;只有使用者明確確認「不適用」並說明原因,才可標記該項完成。
|
||||
|
||||
### 3. 建立前完整性閘門
|
||||
|
||||
套用 `references/requirements-discovery.md` 的完整性閘門。九段落都必須有實質內容,或由使用者確認不適用並說明原因;不得留下未回答或「尚未決定」的缺口。未通過閘門前不得套用模板、查標籤或建立議題。
|
||||
|
||||
### 4. 填入需求議題模板
|
||||
|
||||
套用 `templates/requirement-issue.md`,填入總覽、背景、目標、非目標、領域名詞表、文件、驗收標準、影響範圍與未決事項;全程使用繁體中文。
|
||||
|
||||
### 5. 取得既有標籤
|
||||
|
||||
用 `scripts/labels-list.js` 取得既有標籤,只能選既有標籤。
|
||||
|
||||
### 6. 試跑並建立議題
|
||||
|
||||
寫入前先執行:
|
||||
|
||||
```
|
||||
node scripts/issue-create.js --repo <owner/name> --title "<標題>" --body-file <暫存檔> --labels "<標籤>" --dry-run
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
一個步驟裡只有一半合判準時,**標記照下,並在該步寫明哪一半不委派**。這比整步不標好——
|
||||
不標的話那一半的中間產物照樣塞滿主脈絡;也比整步委派安全,因為第四條是硬排除。
|
||||
|
||||
目前有三步是這個形狀:兩份圖解總覽(產出 HTML 可委派,寫回議題的 `issue-update` 不委派)
|
||||
與分批提交(方案計算可委派,實際跑 `commit-split.js` 不委派)。
|
||||
目前有一步是這個形狀:分批提交的方案計算可委派,實際跑 `commit-split.js` 不委派。
|
||||
|
||||
## 目前標記為〔可委派〕的步驟
|
||||
|
||||
@@ -46,12 +45,11 @@
|
||||
|
||||
| 正本 | 步驟 | 委派範圍 |
|
||||
| --- | --- | --- |
|
||||
| `sdlc-plan` | 產生圖解版總覽 | 產出 HTML;寫回議題不委派 |
|
||||
| `sdlc-plan` | 列出九段落依據與缺漏 | 產出可核對的清單 |
|
||||
| `sdlc-analyze` | 對四份清單列出疑點 | 全步 |
|
||||
| `sdlc-analyze` | 算出截止日 | 全步 |
|
||||
| `sdlc-analyze` | 產生分析版的圖解總覽 | 產出 HTML;寫回議題不委派 |
|
||||
| `sdlc-analyze` | 算出截止日 | 計算日期;寫回議題不委派 |
|
||||
| `sdlc-feat` | 把議題標題翻成英文 | 全步 |
|
||||
| `sdlc-feat` | 分批提交 | 方案計算;實際提交不委派 |
|
||||
| `sdlc-feat` | 分批提交方案 | 方案計算;實際提交不委派 |
|
||||
|
||||
`sdlc-sync`、`sdlc-fix` 與 `sdlc-report` 目前沒有可委派的步驟:前兩者每一步都在問使用者
|
||||
或寫入 Gitea,後者只有一支唯讀腳本,委派出去省不到什麼。
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 需求補全規則
|
||||
|
||||
這份文件是 `/sdlc-plan` 判斷需求內容是否足夠清楚、可驗收的規則正本。
|
||||
|
||||
## 需求的四個必要維度
|
||||
|
||||
每個需要實作的需求都要能回答:
|
||||
|
||||
1. **角色** — 誰需要這個結果,或誰執行這個行為?
|
||||
2. **情境** — 在什麼觸發條件、前置條件或使用情境下發生?
|
||||
3. **行為** — 角色做了什麼,或系統需要處理什麼?
|
||||
4. **可觀察結果** — 外部使用者、呼叫端或驗收者能觀察到什麼結果?
|
||||
|
||||
只寫功能名稱、畫面名稱、API 名稱或實作方法,不足以視為完成。
|
||||
|
||||
## 情境覆蓋
|
||||
|
||||
- 至少確認主要成功情境。
|
||||
- 若需求有權限、取消、重試、空值、重複執行、並行、極大量或外部來源失敗等可能性,逐一確認適用的失敗或邊界情境。
|
||||
- 不適用的情境必須由使用者明確確認「不適用」,並說明原因;agent 不得自行判定。
|
||||
|
||||
## 提問格式
|
||||
|
||||
一次只問一個最高價值的缺口。每題依序提供:
|
||||
|
||||
- **目前理解**:根據輸入與已確認回答整理的一句話。
|
||||
- **缺少內容**:指出哪個必要維度或情境仍不清楚。
|
||||
- **為什麼需要**:說明它會影響哪個目標、範圍或驗收結果。
|
||||
- **建議回答**:提供具體選項或短範例。
|
||||
- **自由回答**:明確允許使用者改寫或提供其他答案。
|
||||
|
||||
建議選項是協助理解,不是替使用者做決策。
|
||||
|
||||
## 查證與提問邊界
|
||||
|
||||
- 能從輸入、既有議題或可取得的 repo 內容查證的事項,先自行查證。
|
||||
- 只有需要需求擁有者決策的事項才提問。
|
||||
- 不把架構、資料表、模組或其他實作方案當成需求答案;這些交給 `/sdlc-analyze`。
|
||||
- 不把 agent 的推測寫成使用者已確認的需求。
|
||||
|
||||
## 回答狀態
|
||||
|
||||
- 具體回答可填入工作稿,並重新檢查所有必要維度與情境。
|
||||
- 使用者明確確認「不適用」且提供原因,可標記該項已完成。
|
||||
- 未回答、拒絕回答、「不知道」或「尚未決定」都仍是缺口,不得填成已確認。
|
||||
- 仍有缺口時不得建立需求議題;應繼續一次問一題。
|
||||
|
||||
## 建立前完整性閘門
|
||||
|
||||
建立需求議題前必須確認:
|
||||
|
||||
- 九個段落都有實質內容,或使用者已確認不適用並說明原因。
|
||||
- 需求的角色、情境、行為與可觀察結果完整。
|
||||
- 主要成功情境已確認;適用的失敗與邊界情境已確認。
|
||||
- 沒有未回答或「尚未決定」的阻塞缺口。
|
||||
- 驗收標準描述外部可觀察結果,不綁定不必要的實作細節。
|
||||
- 內容前後一致,且非目標足以限制範圍。
|
||||
|
||||
通過閘門後才可套用需求議題模板、查標籤與建立議題。
|
||||
@@ -81,7 +81,8 @@ test('打包內容以白名單決定:四個正本目錄都在,測試與暫
|
||||
const packed = JSON.parse(
|
||||
execFileSync('npm', ['pack', '--dry-run', '--json'], { cwd: repoRoot, encoding: 'utf8' }),
|
||||
);
|
||||
const files = packed[0].files.map((file) => file.path);
|
||||
const manifest = Array.isArray(packed) ? packed[0] : packed[Object.keys(packed)[0]];
|
||||
const files = manifest.files.map((file) => file.path);
|
||||
|
||||
for (const dir of ['prompts/', 'scripts/', 'templates/', 'references/', 'bin/']) {
|
||||
assert.ok(
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readReference, readPrompt, assertNeutralPrompt } from './helpers/prompt-doc.js';
|
||||
|
||||
const RULES = readReference('delegation');
|
||||
const PROMPTS = ['sdlc-plan', 'sdlc-analyze', 'sdlc-feat', 'sdlc-fix', 'sdlc-sync', 'sdlc-report'];
|
||||
const EXPECTED = [
|
||||
['sdlc-plan', '列出九段落依據與缺漏'],
|
||||
['sdlc-analyze', '對四份清單列出疑點'],
|
||||
['sdlc-analyze', '算出截止日'],
|
||||
['sdlc-feat', '把議題標題翻成英文'],
|
||||
['sdlc-feat', '分批提交方案'],
|
||||
];
|
||||
|
||||
function tableEntries() {
|
||||
return [...RULES.matchAll(/^\| `([^`]+)` \| ([^|]+) \|/gm)]
|
||||
.map((match) => [match[1], match[2].trim()]);
|
||||
}
|
||||
|
||||
function promptMarkers(name) {
|
||||
const prompt = readPrompt(name);
|
||||
return [...prompt.matchAll(/^### (?:\d+\.\s+)?(.+?)〔可委派〕\s*$/gm)]
|
||||
.map((match) => [name, match[1].trim()]);
|
||||
}
|
||||
|
||||
// ── 四條判準與雙向集合 ────────────────────────────────────────────
|
||||
|
||||
test('委派規則保留四條判準與兩條硬排除', () => {
|
||||
for (const term of ['可驗證的成品', '不會詢問使用者', '失敗能被呼叫端偵測', '不直接寫入 Gitea 或 git']) {
|
||||
assert.match(RULES, new RegExp(term));
|
||||
}
|
||||
});
|
||||
|
||||
test('委派表與六份正本的標記集合完全一致', () => {
|
||||
const table = tableEntries();
|
||||
const markers = PROMPTS.flatMap(promptMarkers);
|
||||
assert.deepEqual(table, EXPECTED);
|
||||
assert.deepEqual(markers, EXPECTED);
|
||||
});
|
||||
|
||||
test('每個標記步驟都說明能力降級與部分委派邊界', () => {
|
||||
for (const [name, step] of EXPECTED) {
|
||||
const prompt = readPrompt(name);
|
||||
const marker = prompt.match(new RegExp(`^### (?:\\d+\\.\\s+)?${step}〔可委派〕\\s*$`, 'm'));
|
||||
assert.ok(marker, `${name} 缺少標記:${step}`);
|
||||
const start = marker.index;
|
||||
const body = prompt.slice(start, prompt.indexOf('\n### ', start + 1) === -1 ? prompt.length : prompt.indexOf('\n### ', start + 1));
|
||||
assert.match(body, /你的環境若能把工作交給子代理|實際執行.*不委派/);
|
||||
}
|
||||
});
|
||||
|
||||
// ── 六份正本的平台與文字契約 ──────────────────────────────────────
|
||||
|
||||
test('六份流程正本均為平台中立且使用正確 description 前綴', () => {
|
||||
for (const name of PROMPTS) assertNeutralPrompt(readPrompt(name), name);
|
||||
});
|
||||
|
||||
test('沒有委派標記的流程仍明確列入規則表的空集合', () => {
|
||||
for (const name of ['sdlc-fix', 'sdlc-sync', 'sdlc-report']) {
|
||||
assert.equal(promptMarkers(name).length, 0, `${name} 不應偷偷出現委派步驟`);
|
||||
}
|
||||
assert.match(RULES, /sdlc-sync.*sdlc-fix.*sdlc-report/);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { repoRoot } from './helpers/run-script.js';
|
||||
|
||||
const files = [
|
||||
...['sdlc-plan', 'sdlc-analyze', 'sdlc-feat', 'sdlc-fix', 'sdlc-sync', 'sdlc-report']
|
||||
.map((name) => join(repoRoot, 'prompts', `${name}.md`)),
|
||||
join(repoRoot, 'references', 'delegation.md'),
|
||||
join(repoRoot, 'AGENTS.md'),
|
||||
join(repoRoot, 'README.md'),
|
||||
join(repoRoot, 'docs', 'adr', '0001-以集中式雜湊路徑的-worktree-隔離平行工作包.md'),
|
||||
join(repoRoot, 'docs', 'adr', '0002-以能力描述而非工具名表達委派.md'),
|
||||
];
|
||||
|
||||
test('流程、規則與文件資產都是合法 UTF-8 且沒有替代字元', () => {
|
||||
for (const path of files) {
|
||||
const bytes = readFileSync(path);
|
||||
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
||||
assert.equal(text.includes('\uFFFD'), false, `${path} 含替代字元`);
|
||||
}
|
||||
});
|
||||
|
||||
test('六份流程正本都以繁體中文 description 開頭', () => {
|
||||
for (const path of files.slice(0, 6)) {
|
||||
const text = readFileSync(path, 'utf8');
|
||||
assert.match(text, /^description: 僅由 \/sdlc-[a-z-]+ 指令叫用。/m, path);
|
||||
}
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { manifest, tmpRoot } from './helpers/run-script.js';
|
||||
import { manifest, pathWithOnly, tmpRoot } from './helpers/run-script.js';
|
||||
import { fakePrompt, makeFakePlugin } from './helpers/fake-plugin.js';
|
||||
import { startStubGitea } from './helpers/stub-gitea.js';
|
||||
|
||||
@@ -114,7 +114,7 @@ test('PATH 上找不到 tea-sdlc 時整體 ok:false,並指出病灶在 PATH
|
||||
const plugin = makeFakePlugin(t, { prompts: PROMPTS });
|
||||
const home = makeHome(t);
|
||||
|
||||
const { code, json } = await inHome(plugin, home)(['install'], { shim: false });
|
||||
const { code, json } = await inHome(plugin, home)(['install'], { shim: false, path: pathWithOnly(['node']) });
|
||||
|
||||
assert.equal(code, 1);
|
||||
assert.equal(json.ok, false);
|
||||
@@ -261,7 +261,7 @@ test('驗證失敗時已經寫好的轉接檔一份都不刪', async (t) => {
|
||||
const home = makeHome(t);
|
||||
|
||||
// 病灶在 PATH,不在轉接檔:刪掉轉接檔只會讓使用者從「有點舊但能用」變成什麼都沒有
|
||||
const { json } = await inHome(plugin, home)(['install'], { shim: false });
|
||||
const { json } = await inHome(plugin, home)(['install'], { shim: false, path: pathWithOnly(['node']) });
|
||||
|
||||
assert.equal(json.ok, false);
|
||||
for (const path of adaptersOf(json)) {
|
||||
|
||||
@@ -176,6 +176,27 @@ test('支援 command 的四個平台產生 command 轉接檔,另外三個產
|
||||
assert.deepEqual(filesUnder(join(home, 'work', '.github')), ['skills/sdlc-plan/SKILL.md']);
|
||||
});
|
||||
|
||||
test('七個平台的轉接檔都保留繁體中文且沒有 UTF-8 亂碼', async (t) => {
|
||||
const plugin = makeFakePlugin(t, { prompts: { 'sdlc-plan': fakePrompt('sdlc-plan') } });
|
||||
const home = makeHome(t, ['claude', 'codex', 'opencode', 'oh-my-pi', 'antigravity', 'kiro', 'copilot']);
|
||||
|
||||
await inHome(plugin, home)(['install']);
|
||||
const files = [
|
||||
join(home, '.claude', 'commands', 'sdlc-plan.md'),
|
||||
join(home, '.codex', 'prompts', 'sdlc-plan.md'),
|
||||
join(home, '.config', 'opencode', 'command', 'sdlc-plan.md'),
|
||||
join(home, '.omp', 'agent', 'commands', 'sdlc-plan.md'),
|
||||
join(home, '.gemini', 'skills', 'sdlc-plan', 'SKILL.md'),
|
||||
join(home, '.kiro', 'skills', 'sdlc-plan', 'SKILL.md'),
|
||||
join(home, 'work', '.github', 'skills', 'sdlc-plan', 'SKILL.md'),
|
||||
];
|
||||
|
||||
for (const path of files) {
|
||||
const text = new TextDecoder('utf-8', { fatal: true }).decode(readFileSync(path));
|
||||
assert.match(text, /僅由 \/sdlc-plan 指令叫用。/);
|
||||
}
|
||||
});
|
||||
|
||||
test('轉接檔內容是一句指向 tea-sdlc 的話,不含任何檔案路徑', async (t) => {
|
||||
const plugin = makeFakePlugin(t, { prompts: PROMPTS, version: '1.2.3' });
|
||||
const home = makeHome(t, ['claude']);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* /sdlc-analyze 的流程正本。
|
||||
*
|
||||
* 這些測試鎖定交付文件確認與排程前置條件;漏掉其中任一個閘門,
|
||||
* agent 就可能在使用者尚未確認時建立工作包或排程。
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
assertNeutralPrompt,
|
||||
assertPromptListsSections,
|
||||
readPrompt,
|
||||
} from './helpers/prompt-doc.js';
|
||||
|
||||
const prompt = readPrompt('sdlc-analyze');
|
||||
const delivery = prompt.slice(prompt.indexOf('### 交付文件判斷'), prompt.indexOf('## 第二段'));
|
||||
const packages = prompt.slice(prompt.indexOf('## 第二段'), prompt.indexOf('## 第三段'));
|
||||
const schedule = prompt.slice(prompt.indexOf('## 第三段'), prompt.indexOf('## 邊界'));
|
||||
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
|
||||
const TYPES = [
|
||||
'需求描述概要',
|
||||
'WBS(工作分解結構)',
|
||||
'流程圖',
|
||||
'甘特圖',
|
||||
'PERT 圖',
|
||||
'關鍵路徑圖',
|
||||
'API 契約文件',
|
||||
];
|
||||
|
||||
// ── 交付文件確認 ────────────────────────────────────────────────
|
||||
|
||||
test('正本讀取七種交付文件規則,且依規則順序逐項確認', () => {
|
||||
assert.match(delivery, /references\/delivery-types\.md/);
|
||||
assertPromptListsSections(delivery, TYPES);
|
||||
assert.match(delivery, /依序逐一詢問/);
|
||||
assert.match(delivery, /不得把七種文件合併成一次確認/);
|
||||
});
|
||||
|
||||
test('每種文件確認都保留必要內容、產出位置與 ELI5 規則', () => {
|
||||
assert.match(delivery, /必要內容骨架/);
|
||||
assert.match(delivery, /產出位置/);
|
||||
assert.match(delivery, /ELI5 變體規則/);
|
||||
});
|
||||
|
||||
test('交付文件未確認或遭拒時,所有後續副作用都被阻擋', () => {
|
||||
assert.match(delivery, /未確認或拒絕任何一項時,立即停止/);
|
||||
assert.match(delivery, /不建立工作包、不排程、不建立相依/);
|
||||
assert.match(delivery, /不寫入 Milestone、看板或其他後續資料/);
|
||||
});
|
||||
|
||||
// ── PERT 與工作包優先 ────────────────────────────────────────────
|
||||
|
||||
test('PERT 的 O、M、P 分別逐題確認,且不能用單一人天替代', () => {
|
||||
const pert = prompt.slice(prompt.indexOf('### PERT 三點估算'), prompt.indexOf('## 第二段'));
|
||||
assert.match(pert, /樂觀時間(O)/);
|
||||
assert.match(pert, /最可能時間(M)/);
|
||||
assert.match(pert, /悲觀時間(P)/);
|
||||
assert.match(pert, /分別逐題詢問/);
|
||||
assert.match(pert, /不得用單一人天估算代替/);
|
||||
assert.match(pert, /三個值都確認後才納入排程資料/);
|
||||
});
|
||||
|
||||
test('交付文件工作包優先,但以先決關係的拓撲順序為準', () => {
|
||||
assert.match(packages, /先放已確認的交付文件工作包,再放純程式碼工作包/);
|
||||
assert.match(packages, /不違反先決關係/);
|
||||
assert.match(packages, /拓撲順序/);
|
||||
});
|
||||
|
||||
test('每個工作包只承擔一種交付並保留驗收所需欄位', () => {
|
||||
assert.match(packages, /每顆工作包只做一種交付/);
|
||||
assert.match(packages, /文件類型、必要內容、產出位置與驗收方式/);
|
||||
assert.match(packages, /對應已確認交付項目的待辦置於第一項/);
|
||||
});
|
||||
|
||||
// ── 建立與排程邊界 ────────────────────────────────────────────────
|
||||
test('重跑需求會以既有議題與標題查重,不建立重複工作包', () => {
|
||||
assert.match(packages, /重跑同一需求時,先以既有議題與標題查重/);
|
||||
assert.match(packages, /不建立重複工作包/);
|
||||
});
|
||||
|
||||
test('工作包建立與排程仍要求 dry-run,且排程在相依確認後', () => {
|
||||
assert.match(packages, /issue-create\.js --dry-run/);
|
||||
assert.match(schedule, /所有工作包建立且相依關係確認後/);
|
||||
assert.match(schedule, /schedule\.js/);
|
||||
assert.match(schedule, /各腳本先 dry-run,再實跑/);
|
||||
});
|
||||
|
||||
test('共識、交付確認與 PERT 完成前不建立任何後續資料', () => {
|
||||
assert.match(boundary, /共識摘要、交付文件確認與 PERT 三點估算完成前/);
|
||||
assert.match(boundary, /不建立任何工作包、不排程、不寫入後續資料/);
|
||||
});
|
||||
|
||||
test('API 契約不寫入目標專案,且流程維持終端摘要與平台中立', () => {
|
||||
assert.match(delivery, /API 契約文件只能交付預覽或使用者確認的位置/);
|
||||
assert.match(delivery, /禁止寫入目標專案 repo/);
|
||||
assert.match(prompt, /摘要只印終端,不寫入 Gitea/);
|
||||
assert.match(boundary, /不把 API 契約文件寫入目標專案 repo/);
|
||||
assertNeutralPrompt(prompt, 'sdlc-analyze');
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { assertNeutralPrompt, readPrompt } from './helpers/prompt-doc.js';
|
||||
|
||||
const prompt = readPrompt('sdlc-feat');
|
||||
const 分流 = prompt.slice(prompt.indexOf('## 交付文件待辦分流'), prompt.indexOf('## 實作與交付'));
|
||||
|
||||
// ── 正本基本契約 ───────────────────────────────────────────────────
|
||||
|
||||
test('正本平台中立,description 前綴正確', () => {
|
||||
assertNeutralPrompt(prompt, 'sdlc-feat');
|
||||
});
|
||||
|
||||
test('輸入先抽取工作包並保留留言接回流程', () => {
|
||||
assert.match(prompt, /scripts\/wp-extract\.js/);
|
||||
assert.match(prompt, /未處理留言數/);
|
||||
assert.match(prompt, /重新抽取一次/);
|
||||
});
|
||||
|
||||
test('保留 worktree、依賴、驗收與 PR 的既有交付流程', () => {
|
||||
assert.match(prompt, /依工作包 `repos` 逐一準備 worktree/);
|
||||
assert.match(prompt, /立即勾選對應驗收/);
|
||||
assert.match(prompt, /scripts\/pr-create\.js --dry-run/);
|
||||
});
|
||||
|
||||
// ── 交付文件分流 ───────────────────────────────────────────────────
|
||||
|
||||
test('交付文件待辦與程式碼待辦走不同流程', () => {
|
||||
assert.match(分流, /判定為程式碼待辦或交付文件待辦/);
|
||||
assert.match(分流, /不可共用無差別的實作流程/);
|
||||
assert.match(分流, /references\/delivery-types\.md/);
|
||||
});
|
||||
|
||||
test('每份文件產出前逐一列骨架並一次確認一題', () => {
|
||||
assert.match(分流, /每一份交付文件都要分開處理/);
|
||||
assert.match(分流, /必要內容骨架、已知來源、未決事項、建議與理由/);
|
||||
assert.match(分流, /一次只問一題確認內容是否齊全/);
|
||||
assert.match(分流, /未確認的文件不可標成已交付/);
|
||||
});
|
||||
|
||||
test('預覽能力以可開啟可分享能力判斷,具備時直接產出', () => {
|
||||
assert.match(分流, /即時發佈成可開啟、可分享的預覽頁/);
|
||||
assert.match(分流, /具備能力後直接產出預覽/);
|
||||
assert.match(分流, /回報實際位置與內容摘要/);
|
||||
});
|
||||
|
||||
test('預覽能力不足時一次一題詢問,建議由情境推導', () => {
|
||||
assert.match(分流, /無法證明具備能力、預覽不可開啟或不可分享/);
|
||||
assert.match(分流, /一次只問一題/);
|
||||
assert.match(分流, /依文件、來源與限制提出建議及理由/);
|
||||
assert.match(分流, /保留手動輸入/);
|
||||
assert.match(分流, /不替使用者決定交付方式/);
|
||||
assert.match(分流, /不要靠固定清單或猜測環境/);
|
||||
assert.match(prompt, /不產生固定選項清單/);
|
||||
});
|
||||
|
||||
// ── ELI5 與契約邊界 ─────────────────────────────────────────────────
|
||||
|
||||
test('ELI5 保留技術意義並把圖表改成圖片式視覺', () => {
|
||||
assert.match(分流, /保留原文件的範圍、順序、相依、例外與驗收意義/);
|
||||
assert.match(分流, /不刪除技術限制/);
|
||||
assert.match(分流, /重新繪製成容易閱讀的圖片式視覺/);
|
||||
assert.match(分流, /不把 Mermaid 原碼當成交付物/);
|
||||
});
|
||||
|
||||
test('API 契約只在預覽或確認位置交付,不寫入目標 repo', () => {
|
||||
assert.match(分流, /API 契約文件只能交付在預覽位置或使用者確認的位置/);
|
||||
assert.match(分流, /不得寫入目標專案 repo/);
|
||||
assert.match(分流, /不捏造網址/);
|
||||
});
|
||||
Reference in New Issue
Block a user