From 35bafcc928478d25209405cb4d3f2a76a3594279 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:43:24 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat(=E5=B7=A5=E4=BD=9C=E5=8C=85):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B7=A5=E4=BD=9C=E5=8C=85=E8=AD=B0=E9=A1=8C?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 九個段落與順序:這個工作包在做什麼/描述/架構圖/範圍邊界/介面契約/待辦/ 整體驗收/repo 列表/關聯。段落順序即下游 wp-extract 的解析依據。 介面契約是四欄表格(介面/產出者/消費者/形狀)並帶分隔列,讓解析有明確的 資料列起點。架構圖同樣不寫死 mermaid 圍欄——正本允許不畫,圍欄寫死時不畫會在 議題頁留下一塊渲染失敗的空區塊。 Co-Authored-By: Claude Opus 5 (1M context) --- templates/work-package-issue.md | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 templates/work-package-issue.md diff --git a/templates/work-package-issue.md b/templates/work-package-issue.md new file mode 100644 index 0000000..6733189 --- /dev/null +++ b/templates/work-package-issue.md @@ -0,0 +1,37 @@ +## 這個工作包在做什麼 + +{{一句話}} + +## 描述 + +{{描述}} + +## 架構圖 + +{{架構圖}} + +## 範圍邊界 + +{{範圍邊界}} + +## 介面契約 + +| 介面 | 產出者 | 消費者 | 形狀 | +| --- | --- | --- | --- | +{{介面契約}} + +## 待辦 + +{{待辦}} + +## 整體驗收 + +{{整體驗收}} + +## repo 列表 + +{{repo列表}} + +## 關聯 + +{{關聯}} -- 2.53.0 From 0cd25d49d4844e1bfefc9efb4a9dd74370a69e45 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:43:24 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat(sdlc-analyze):=20=E6=93=B4=E5=85=85?= =?UTF-8?q?=E7=82=BA=E5=85=A9=E6=AE=B5=E5=BC=8F=EF=BC=8C=E7=AC=AC=E4=BA=8C?= =?UTF-8?q?=E6=AE=B5=E6=8A=8A=E5=85=B1=E8=AD=98=E8=AE=8A=E6=88=90=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=85=E8=AD=B0=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第一段到共識摘要為止仍然完全不寫入;使用者點頭之後才進入第二段建立議題。 邊界條文隨之改寫成「共識摘要之前不對 Gitea 產生任何寫入」,並明列第二段 不做的事:不建相依、不掛 Milestone、不加看板、不寫人天估算——那些是後續 流程的工作,寫在這裡會讓兩顆工作包互相踩。 工作包的切法、標題規則(動詞加名詞、禁止 WP-01 這類流水編號)、待辦與驗收 的巢狀寫法(附可照抄的範例)、架構圖依性質三選一,都在這一段定下來。 邊界的斷言跟著條文一起改,因為條文換了語意;分開成兩顆 commit 的話中間那顆 會是紅的。另補一條斷言:第一段不得出現任何寫入型腳本的名字。 Closes #7 Co-Authored-By: Claude Opus 5 (1M context) --- prompts/sdlc-analyze.md | 91 +++++++++++++++++++++++++++++--- test/sdlc-analyze-assets.test.js | 11 +++- 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/prompts/sdlc-analyze.md b/prompts/sdlc-analyze.md index 5ae2695..7c9b840 100644 --- a/prompts/sdlc-analyze.md +++ b/prompts/sdlc-analyze.md @@ -1,11 +1,12 @@ name: sdlc-analyze -description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可行性檢查,把疑點逐題問到共識並輸出摘要。 +description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可行性檢查,逐題問到共識後產生工作包議題。 # sdlc-analyze -對一顆需求議題執行可行性檢查,把疑點一題一題問到雙方有共識。 +對一顆需求議題執行可行性檢查,把疑點一題一題問到雙方有共識,再把共識變成一批工作包議題。 -這一段到共識摘要為止,**不寫入 Gitea**。把工作包開出去是下一段的事。 +分成兩段:**可行性分析**到共識摘要為止,完全不寫入 Gitea;使用者看過摘要點頭之後, +才進入**產生工作包**,那一段才會建立議題。 這份檔案是流程正本。各平台的轉接檔只是指回這裡,不要把規則抄過去。 @@ -13,7 +14,7 @@ description: 僅由 /sdlc-analyze 指令叫用。對一顆需求議題執行可 一個需求議題編號。 -## 步驟 +## 第一段:可行性分析 ### 1. 讀議題 @@ -66,9 +67,87 @@ node scripts/issue-extract.js --repo --index <編號> 摘要只印在終端,**不寫回議題、不建立任何東西**。使用者看過點頭之後,才進入下一段。 +## 第二段:產生工作包 + +**使用者對共識摘要點頭之後才開始。** 摘要沒有經過確認就不要往下走。 + +### 5. 切出工作包 + +把需求切成幾顆工作包。一顆工作包是**開發者拿了就能動手、做完有明確結果**的單位: +它有自己的驗收標準,做完能單獨被檢視,不必等別的工作包一起才看得出成果。 + +切的依據是第一段問出來的共識,特別是時程清單那份暫定拆法——那本來就是這一段的草稿。 + +**標題格式為「{動詞}{名詞}」**,例如「建立工作包的抽取契約」、「產生圖解版總覽網頁」。 +**禁止流水編號與任何無意義代號**(`WP-01`、`任務三`、`第一階段`):命名本身就要說明用途, +看標題就知道這顆在做什麼,不必點進去。 + +### 6. 組出每顆工作包的內容 + +套用 `templates/work-package-issue.md`,依序填滿九個段落: + +1. **這個工作包在做什麼** — 一句話。讓人掃過標題與這一行就決定要不要點進來。 +2. **描述** — 從使用者的角度說這顆做完之後什麼事變得可能,不要寫成逐層的實作清單。 +3. **架構圖** — 見下方「架構圖的限制」。 +4. **範圍邊界** — 明列**不做什麼**。這一段的用途是抵抗範圍蔓延,寫得越具體越有用。 +5. **介面契約** — 表格,四欄:介面/產出者/消費者/形狀。讓人知道自己產出的東西誰會消費。 + 這顆不產出對外介面就寫一列「無」,不要留空表。 +6. **待辦** — 巢狀結構:每一項待辦底下掛**它自己的**驗收標準,讓人知道這一項做到什麼程度算完成。 + + ``` + - [ ] 建立共用函式庫 + - [ ] 具名 flag 解析可拒絕未知參數 + - [ ] 單行 JSON 輸出格式固定 + - [ ] 加上前置檢查 + - [ ] 四層各自回傳可區分的錯誤碼 + ``` + + 上層是待辦、縮排一層是該項的驗收,不要再往下巢狀。兩者都用 checkbox,實作時會被逐項勾選。 +7. **整體驗收** — 整顆工作包做完才驗得出來的事,與個別待辦的驗收不重複。 +8. **repo 列表** — 這顆會動到哪些 repo。 +9. **關聯** — 至少要有一行 `需求議題:#<編號>` 指回來源。阻擋、先決與人天估算由後續流程補上。 + +### 7. 先試跑,再寫入 + +每顆工作包各寫一個暫存檔,然後逐顆: + +``` +node scripts/issue-create.js --repo --title "<標題>" --body-file <暫存檔> \ + --labels "<標籤>" --dry-run +``` + +`--dry-run` 會印出將送出的請求、把標籤名稱換成 id,並在標題已存在時如實顯示「實跑會是 +no-op」。確認無誤後拿掉該旗標再跑一次。 + +標籤一樣只能從 `scripts/labels-list.js` 回傳的既有標籤裡挑,**不得自行建立新標籤**。 + +中斷後重跑不會產生重複工作包:`issue-create` 以標題查重,發現同名議題就回傳既有那一顆 +並把 `created` 設為 `false`。 + +### 8. 回報 + +列出每顆工作包的編號、標題與網址。不要把議題內容再貼一次。 + +## 架構圖的限制 + +依工作包的性質選圖: + +- **`sequenceDiagram`** — 重點在「誰呼叫誰、順序為何」時用。 +- **`flowchart`** — 重點在「條件分支與資料流向」時用。 +- **`stateDiagram-v2`** — 重點在「狀態怎麼轉移」時用。 + +節點數上限 **12**,每個節點的文字上限 **8 字**。超過就拆成多張圖,或者乾脆不畫。 + +模板的 `{{架構圖}}` 要填入**完整的內容**,兩種形式擇一: + +- 要畫:一個或多個完整的 ```mermaid 圍欄區塊。 +- 不畫:一行說明為什麼不畫,**不要加圍欄**。 + ## 邊界 -- 不對 Gitea 產生任何寫入:不建議題、不改描述、不貼標籤、不留留言。 +- **共識摘要之前不對 Gitea 產生任何寫入**:不建議題、不改描述、不貼標籤、不留留言。 +- 第二段只建立工作包議題。不建相依、不掛 Milestone、不加看板、不寫人天估算——那是後續流程的事。 +- 不自行建立標籤、Milestone 或專案看板。 - 不修改使用者的專案檔案。查證既有功能時只讀不寫。 - 不替使用者決定他沒回答的事。問不到答案就進「仍然未決的事」。 -- 不自行建立標籤、Milestone 或專案看板。 +- 不關閉或刪除任何既有議題。 diff --git a/test/sdlc-analyze-assets.test.js b/test/sdlc-analyze-assets.test.js index 7e69ef2..a5c4ab9 100644 --- a/test/sdlc-analyze-assets.test.js +++ b/test/sdlc-analyze-assets.test.js @@ -76,13 +76,20 @@ test('正本規定最後輸出共識摘要,且摘要只印不寫', () => { assert.match(prompt, /不寫回議題/); }); -test('正本把「不對 Gitea 寫入」寫成明確邊界', () => { +test('正本把「共識摘要之前不寫入」寫成明確邊界', () => { const boundary = prompt.slice(prompt.indexOf('## 邊界')); - assert.match(boundary, /不對 Gitea 產生任何寫入/); + assert.match(boundary, /共識摘要之前不對 Gitea 產生任何寫入/); assert.match(boundary, /不建議題/); assert.match(boundary, /不留留言/); }); +test('第一段在共識摘要之前不叫用任何寫入型腳本', () => { + const analysis = prompt.slice(prompt.indexOf('## 第一段'), prompt.indexOf('## 第二段')); + for (const writer of ['issue-create', 'issue-update', 'issue-link', 'project-add', 'timer']) { + assert.equal(analysis.includes(writer), false, `第一段不該出現寫入型腳本 ${writer}`); + } +}); + test('正本要求先看未處理留言數,不是 0 就提示先整併', () => { assert.match(prompt, /未處理留言數/); assert.match(prompt, /sdlc-sync/); -- 2.53.0 From f0beb1ff643a3981875c5045531169d852a966ab Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:43:25 +0000 Subject: [PATCH 3/6] =?UTF-8?q?test(=E5=B7=A5=E4=BD=9C=E5=8C=85):=20?= =?UTF-8?q?=E9=87=98=E4=BD=8F=E6=A8=A1=E6=9D=BF=E8=88=87=E3=80=8C=E7=94=A2?= =?UTF-8?q?=E7=94=9F=E5=B7=A5=E4=BD=9C=E5=8C=85=E3=80=8D=E9=82=A3=E4=B8=80?= =?UTF-8?q?=E6=AE=B5=E7=9A=84=E8=A6=8F=E5=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模板的段落順序決定 wp-extract 解析得到什麼,待辦的巢狀寫法決定實作階段勾得到 哪一行——這兩件事寫死在測試裡,改動時才會被逼著一起改。 巢狀範例的斷言不比對固定縮排量,而是比對「最深的一層比最淺的深」,這樣重排 外層清單的縮排不會弄壞測試。 Co-Authored-By: Claude Opus 5 (1M context) --- test/work-package-assets.test.js | 131 +++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 test/work-package-assets.test.js diff --git a/test/work-package-assets.test.js b/test/work-package-assets.test.js new file mode 100644 index 0000000..e7416b3 --- /dev/null +++ b/test/work-package-assets.test.js @@ -0,0 +1,131 @@ +/** + * 工作包議題的模板,以及正本裡「產生工作包」那一段的規則。 + * + * 模板的段落順序決定 #9 的 wp-extract 解析得到什麼;待辦的巢狀寫法決定實作階段 + * 勾得到哪一行。這兩件事寫死在測試裡,改動時才會被逼著一起改。 + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readPrompt, readTemplate } from './helpers/prompt-doc.js'; + +const template = readTemplate('work-package-issue'); +const prompt = readPrompt('sdlc-analyze'); +/** 第二段的內容,避免把第一段的字樣誤認成這一段的規則 */ +const phase2 = prompt.slice(prompt.indexOf('## 第二段')); + +/** 工作包議題的九個段落,順序即議題裡的順序 */ +const SECTIONS = [ + '這個工作包在做什麼', + '描述', + '架構圖', + '範圍邊界', + '介面契約', + '待辦', + '整體驗收', + 'repo 列表', + '關聯', +]; + +// ── 輸出模板 ─────────────────────────────────────────────────────── + +test('模板依序包含九個段落', () => { + const headings = [...template.matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()); + assert.deepEqual(headings, SECTIONS); +}); + +test('模板每個段落都有 {{變數}} 佔位', () => { + const placeholders = [...template.matchAll(/\{\{([^}]+)\}\}/g)]; + assert.equal(placeholders.length, SECTIONS.length); +}); + +test('介面契約是四欄表格:介面/產出者/消費者/形狀', () => { + const section = template.slice(template.indexOf('## 介面契約'), template.indexOf('## 待辦')); + assert.match(section, /\|\s*介面\s*\|\s*產出者\s*\|\s*消費者\s*\|\s*形狀\s*\|/); + assert.match(section, /\|\s*---\s*\|/, '要有分隔列,wp-extract 以它為界找資料列'); +}); + +test('模板不把 mermaid 圍欄寫死:不畫圖時才不會留下渲染失敗的空區塊', () => { + assert.equal(template.includes('```mermaid'), false); + const section = template.slice(template.indexOf('## 架構圖'), template.indexOf('## 範圍邊界')); + assert.match(section.trim(), /^## 架構圖\s+\{\{架構圖\}\}$/); +}); + +// ── 產生工作包那一段 ─────────────────────────────────────────────── + +test('第二段要等使用者對共識摘要點頭才開始', () => { + assert.match(phase2, /點頭之後才開始/); + assert.match(phase2, /沒有經過確認就不要往下走/); +}); + +test('正本逐一交代九個段落,且順序與模板一致', () => { + const listed = [...phase2.matchAll(/^\d+\.\s+\*\*(.+?)\*\*/gm)].map((m) => m[1].trim()); + assert.deepEqual(listed, SECTIONS); +}); + +test('標題規則為動詞加名詞,且明令禁止流水編號', () => { + assert.match(phase2, /\{動詞\}\{名詞\}/); + assert.match(phase2, /禁止流水編號/); + assert.match(phase2, /WP-01/, '要舉出被禁止的寫法,不要只說「不要用編號」'); +}); + +test('待辦的巢狀寫法有具體範例,且說明上層與縮排各代表什麼', () => { + const example = phase2.match(/```[^\n]*\n([\s\S]*?)```/)?.[1] ?? ''; + const indents = example + .split('\n') + .filter((line) => line.trim().startsWith('- [')) + .map((line) => line.match(/^\s*/)[0].length); + + assert.ok(indents.length >= 3, '範例要有數行待辦才看得出結構'); + assert.ok( + Math.max(...indents) > Math.min(...indents), + '範例要真的有縮排出來的巢狀層,不能只用文字描述', + ); + assert.match(phase2, /上層是待辦、縮排一層是該項的驗收/); + assert.match(phase2, /不要再往下巢狀/); +}); + +test('範圍邊界要求明列不做什麼', () => { + assert.match(phase2, /明列\*\*不做什麼\*\*/); + assert.match(phase2, /抵抗範圍蔓延/); +}); + +test('介面契約段落交代了沒有對外介面時怎麼填', () => { + assert.match(phase2, /不要留空表/); +}); + +test('關聯段落必須指回來源需求議題', () => { + assert.match(phase2, /需求議題:#/); +}); + +test('架構圖依性質三選一,並釘住節點與字數上限', () => { + for (const kind of ['sequenceDiagram', 'flowchart', 'stateDiagram-v2']) { + assert.match(prompt, new RegExp(kind)); + } + const limits = prompt.slice(prompt.indexOf('## 架構圖的限制')); + assert.match(limits, /12/); + assert.match(limits, /8\s*字/); + assert.match(limits, /拆成多張圖|不畫/); +}); + +test('寫入前先試跑,且試跑的價值有被說明', () => { + assert.match(phase2, /--dry-run/); + assert.match(phase2, /no-op/, '要說明試跑會顯示「實跑是 no-op」,否則使用者不知道該看什麼'); +}); + +test('標籤只能從既有標籤挑,且指名用 labels-list 取得', () => { + assert.match(phase2, /labels-list/); + assert.match(phase2, /不得自行建立新標籤/); +}); + +test('冪等查重有被交代:重跑不會產生第二顆', () => { + assert.match(phase2, /重跑不會產生重複工作包/); + assert.match(phase2, /created.*false|`created` 設為 `false`/); +}); + +test('第二段明列它「不做」的事,避免搶走後續流程的工作', () => { + const boundary = prompt.slice(prompt.indexOf('## 邊界')); + assert.match(boundary, /不建相依/); + assert.match(boundary, /不掛 Milestone/); + assert.match(boundary, /不加看板/); + assert.match(boundary, /不寫人天估算/); +}); -- 2.53.0 From 7b54d566e29f491caab8cadf3ee3f61b0f12e096 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:43:25 +0000 Subject: [PATCH 4/6] =?UTF-8?q?docs(=E8=AD=B0=E9=A1=8C=E8=A7=A3=E6=9E=90):?= =?UTF-8?q?=20=E6=A8=99=E6=98=8E=20tableSection=20=E5=8F=AA=E8=99=95?= =?UTF-8?q?=E7=90=86=E5=85=A9=E6=AC=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工作包的介面契約是四欄(介面/產出者/消費者/形狀),直接沿用這一支會無聲 丟掉第三、四欄。把限制寫在函式註解上,讓接手 wp-extract 的人一眼看到,而不是 自己踩一次才發現。 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/issue-body.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/issue-body.js b/scripts/issue-body.js index 94f8eeb..f1cc834 100644 --- a/scripts/issue-body.js +++ b/scripts/issue-body.js @@ -102,6 +102,10 @@ export function listSection(sections, name) { /** * 取出兩欄表格型段落。以分隔列(|---|---|)為界,之後才是資料列; * 沒有分隔列就當成沒有資料,避免把表頭當成一筆名詞。 + * + * 只處理兩欄:多出來的欄會被丟掉。目前唯一的使用者是需求議題的領域名詞表。 + * 工作包的介面契約是四欄(介面/產出者/消費者/形狀),wp-extract 需要另一個 + * 保留全部欄位的版本,不能直接沿用這一支。 * @param {Map} sections * @param {string} name * @returns {{term: string, def: string}[]} -- 2.53.0 From 9a67c8074fcfe7ecadee196ccbc9f664c01968c1 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:43:26 +0000 Subject: [PATCH 5/6] =?UTF-8?q?fix(=E6=B5=81=E7=A8=8B=E6=AD=A3=E6=9C=AC):?= =?UTF-8?q?=20=E3=80=8C=E4=B8=8D=E7=95=AB=E5=9C=96=E3=80=8D=E7=B6=81?= =?UTF-8?q?=E5=9B=9E=E4=B8=8A=E9=99=90=EF=BC=8C=E4=B8=8D=E6=98=AF=E5=85=8D?= =?UTF-8?q?=E6=AD=BB=E9=87=91=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 兩份正本原本都只寫「不畫:一行說明為什麼不畫」,讀起來像是隨時可以跳過。 議題 #1 的原意是「超過上限即拆圖或不畫」——退路是給畫不下的情況用的。 改成只在超過上限拆不開、或畫了不會比文字更清楚時才選,兩份正本一起改, 免得兩邊講法不同。 Co-Authored-By: Claude Opus 5 (1M context) --- prompts/sdlc-analyze.md | 2 +- prompts/sdlc-plan.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prompts/sdlc-analyze.md b/prompts/sdlc-analyze.md index 7c9b840..5bbbf12 100644 --- a/prompts/sdlc-analyze.md +++ b/prompts/sdlc-analyze.md @@ -141,7 +141,7 @@ no-op」。確認無誤後拿掉該旗標再跑一次。 模板的 `{{架構圖}}` 要填入**完整的內容**,兩種形式擇一: - 要畫:一個或多個完整的 ```mermaid 圍欄區塊。 -- 不畫:一行說明為什麼不畫,**不要加圍欄**。 +- 不畫:**只在超過上限拆不開、或畫了不會比文字更清楚時**才選這個,填一行說明為什麼不畫,**不要加圍欄**。 ## 邊界 diff --git a/prompts/sdlc-plan.md b/prompts/sdlc-plan.md index 661e2f5..e67f6fa 100644 --- a/prompts/sdlc-plan.md +++ b/prompts/sdlc-plan.md @@ -84,7 +84,7 @@ node scripts/issue-create.js --repo --title "<標題>" --body-file 模板的 `{{流程圖}}` 要填入**完整的內容**,兩種形式擇一: - 要畫:一個或多個完整的 ```mermaid 圍欄區塊。 -- 不畫:一行說明為什麼不畫(例如「流程為單一直線,畫圖無助理解」),**不要加圍欄**。 +- 不畫:**只在超過上限拆不開、或畫了不會比文字更清楚時**才選這個,填一行說明為什麼不畫(例如「流程為單一直線,畫圖無助理解」),**不要加圍欄**。 圍欄寫在填入的內容裡而不是模板裡,否則不畫圖時會留下一個空的 mermaid 區塊, 在議題頁上是一塊渲染失敗的紅字。 -- 2.53.0 From 6457d688742fd25a18068bbc4ec94689429afc5c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:43:26 +0000 Subject: [PATCH 6/6] =?UTF-8?q?refactor(=E6=B8=AC=E8=A9=A6):=20=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=B5=90=E6=A7=8B=E7=9A=84=E4=B8=89=E9=A0=85=E6=AA=A2?= =?UTF-8?q?=E6=9F=A5=E6=8A=BD=E5=88=B0=E5=85=B1=E7=94=A8=E6=96=B7=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 段落順序、正本的編號清單、圖表段落不寫死圍欄——這三件事需求議題與工作包議題 都要驗,第二份模板出現時就該抽出來。兩份測試原本各抄一份,其中佔位數的斷言 還悄悄長成不同寫法(一邊 >=、一邊 ==);抽出來之後這種分歧會被逼著講清楚。 Co-Authored-By: Claude Opus 5 (1M context) --- test/helpers/prompt-doc.js | 30 ++++++++++++++++++++++++++++++ test/sdlc-plan-assets.test.js | 19 +++++++++++-------- test/work-package-assets.test.js | 23 +++++++++++++++-------- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/test/helpers/prompt-doc.js b/test/helpers/prompt-doc.js index 292bd54..8b405b3 100644 --- a/test/helpers/prompt-doc.js +++ b/test/helpers/prompt-doc.js @@ -60,3 +60,33 @@ export function assertNeutralPrompt(prompt, command) { assert.equal(prompt.includes(token), false, `正本不該出現平台專屬字樣:${token}`); } } + +/** + * 斷言模板的 `## 標題` 就是這組段落,順序一致。 + * 段落順序即下游抽取契約的解析依據,兩邊必須一起改。 + */ +export function assertTemplateSections(template, sections) { + const headings = [...template.matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()); + assert.deepEqual(headings, sections); +} + +/** + * 斷言正本的編號清單逐一交代了這組段落,順序與模板一致。 + * @param {string} text 正本內容,或其中相關的一段 + */ +export function assertPromptListsSections(text, sections) { + const listed = [...text.matchAll(/^\d+\.\s+\*\*(.+?)\*\*/gm)].map((m) => m[1].trim()); + assert.deepEqual(listed, sections); +} + +/** + * 斷言圖表段落只有佔位、沒有寫死的 mermaid 圍欄。 + * 正本允許「乾脆不畫」,圍欄寫死在模板裡的話,不畫時會在議題頁留下一塊渲染失敗的空區塊。 + * @param {string} heading 圖表段落的標題,例如 '流程圖' + * @param {string} nextHeading 其後一個段落的標題,用來框出範圍 + */ +export function assertDiagramPlaceholderOnly(template, heading, nextHeading) { + assert.equal(template.includes('```mermaid'), false); + const section = template.slice(template.indexOf(`## ${heading}`), template.indexOf(`## ${nextHeading}`)); + assert.match(section.trim(), new RegExp(`^## ${heading}\\s+\\{\\{${heading}\\}\\}$`)); +} diff --git a/test/sdlc-plan-assets.test.js b/test/sdlc-plan-assets.test.js index c3add9f..d18ed26 100644 --- a/test/sdlc-plan-assets.test.js +++ b/test/sdlc-plan-assets.test.js @@ -7,7 +7,14 @@ */ import test from 'node:test'; import assert from 'node:assert/strict'; -import { assertNeutralPrompt, readPrompt, readTemplate } from './helpers/prompt-doc.js'; +import { + assertDiagramPlaceholderOnly, + assertNeutralPrompt, + assertPromptListsSections, + assertTemplateSections, + readPrompt, + readTemplate, +} from './helpers/prompt-doc.js'; const template = readTemplate('requirement-issue'); const prompt = readPrompt('sdlc-plan'); @@ -28,8 +35,7 @@ const SECTIONS = [ // ── 輸出模板 ─────────────────────────────────────────────────────── test('模板依序包含九個段落', () => { - const headings = [...template.matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()); - assert.deepEqual(headings, SECTIONS); + assertTemplateSections(template, SECTIONS); }); test('模板以 {{變數}} 佔位,不留任何空白待填欄位', () => { @@ -82,14 +88,11 @@ test('正本指名由 issue-create 寫入,並提醒先以 --dry-run 檢查', ( test('正本逐一交代九個段落,且順序與模板一致', () => { // 只看「組出議題內容」那份編號清單,不看散落在行文裡的提及 - const listed = [...prompt.matchAll(/^\d+\.\s+\*\*(.+?)\*\*/gm)].map((m) => m[1].trim()); - assert.deepEqual(listed, SECTIONS); + assertPromptListsSections(prompt, SECTIONS); }); test('模板不把 mermaid 圍欄寫死:不畫圖時才不會留下渲染失敗的空區塊', () => { - assert.equal(template.includes('```mermaid'), false); - const section = template.slice(template.indexOf('## 流程圖'), template.indexOf('## 驗收標準')); - assert.match(section.trim(), /^## 流程圖\s+\{\{流程圖\}\}$/); + assertDiagramPlaceholderOnly(template, '流程圖', '驗收標準'); }); test('正本交代了畫與不畫兩種情況各該填什麼', () => { diff --git a/test/work-package-assets.test.js b/test/work-package-assets.test.js index e7416b3..fa398e0 100644 --- a/test/work-package-assets.test.js +++ b/test/work-package-assets.test.js @@ -6,7 +6,13 @@ */ import test from 'node:test'; import assert from 'node:assert/strict'; -import { readPrompt, readTemplate } from './helpers/prompt-doc.js'; +import { + assertDiagramPlaceholderOnly, + assertPromptListsSections, + assertTemplateSections, + readPrompt, + readTemplate, +} from './helpers/prompt-doc.js'; const template = readTemplate('work-package-issue'); const prompt = readPrompt('sdlc-analyze'); @@ -29,8 +35,7 @@ const SECTIONS = [ // ── 輸出模板 ─────────────────────────────────────────────────────── test('模板依序包含九個段落', () => { - const headings = [...template.matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()); - assert.deepEqual(headings, SECTIONS); + assertTemplateSections(template, SECTIONS); }); test('模板每個段落都有 {{變數}} 佔位', () => { @@ -45,9 +50,7 @@ test('介面契約是四欄表格:介面/產出者/消費者/形狀', () }); test('模板不把 mermaid 圍欄寫死:不畫圖時才不會留下渲染失敗的空區塊', () => { - assert.equal(template.includes('```mermaid'), false); - const section = template.slice(template.indexOf('## 架構圖'), template.indexOf('## 範圍邊界')); - assert.match(section.trim(), /^## 架構圖\s+\{\{架構圖\}\}$/); + assertDiagramPlaceholderOnly(template, '架構圖', '範圍邊界'); }); // ── 產生工作包那一段 ─────────────────────────────────────────────── @@ -58,8 +61,7 @@ test('第二段要等使用者對共識摘要點頭才開始', () => { }); test('正本逐一交代九個段落,且順序與模板一致', () => { - const listed = [...phase2.matchAll(/^\d+\.\s+\*\*(.+?)\*\*/gm)].map((m) => m[1].trim()); - assert.deepEqual(listed, SECTIONS); + assertPromptListsSections(phase2, SECTIONS); }); test('標題規則為動詞加名詞,且明令禁止流水編號', () => { @@ -129,3 +131,8 @@ test('第二段明列它「不做」的事,避免搶走後續流程的工作', assert.match(boundary, /不加看板/); assert.match(boundary, /不寫人天估算/); }); + +test('「不畫圖」是有條件的退路,不是免死金牌', () => { + const limits = prompt.slice(prompt.indexOf('## 架構圖的限制')); + assert.match(limits, /只在超過上限拆不開、或畫了不會比文字更清楚時/); +}); -- 2.53.0