Merge pull request 'feat/delivery-document-contract/main' (#80) from feat/delivery-document-contract/main into master
Reviewed-on: #80 Reviewed-by: 系統管理員 <1+admin@noreply.localhost>
This commit was merged in pull request #80.
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
# 交付類型規則
|
||||||
|
|
||||||
|
這份規則是 `/sdlc-analyze` 與 `/sdlc-feat` 共用的交付文件正本。交付文件不是額外的程式碼待辦;先確認要交付哪一種文件,再依本表逐一確認內容骨架,最後才產出。
|
||||||
|
|
||||||
|
## 共通規則
|
||||||
|
|
||||||
|
- 每一種文件都要逐一確認:說明是否需要、列出必要內容骨架、給出建議與理由,再接受使用者確認或手動調整;不得把七種文件合併成一次模糊確認。
|
||||||
|
- 文件沒有足夠資料時標記未決事項,不代替使用者編造決策。能由需求、工作包、相依與排程資料重新推導的內容,優先保留來源與推導規則。
|
||||||
|
- 產出位置以本表為準。預覽能力存在時可交付可開啟、可分享的預覽;沒有預覽能力時依使用者確認的方式交付,不因缺少預覽而捏造網址或改寫目標專案。
|
||||||
|
- ELI5 變體要保留原文件的範圍、順序、相依、例外與驗收意義。可把術語換成日常說法並補一句解釋,但不能刪掉技術限制;圖表改成容易閱讀的視覺,不把 Mermaid 原碼當成 ELI5 交付物。
|
||||||
|
|
||||||
|
## 七種交付類型
|
||||||
|
|
||||||
|
### 1. 需求描述概要
|
||||||
|
|
||||||
|
- **必要內容**:一句話說明做什麼與為什麼做;背景;目標與可驗收結果;非目標;影響範圍;假設與未決事項;必要的領域名詞定義。
|
||||||
|
- **產出位置**:需求議題的結構化描述,對應 `templates/requirement-issue.md` 的段落;可另外提供預覽,但議題內仍保留可機讀的白話概要。
|
||||||
|
- **ELI5 變體**:先用一句日常語言說明問題和得到的改善,再用短句解釋必要術語;不得用願景口號取代目標、非目標或驗收條件。
|
||||||
|
|
||||||
|
### 2. WBS(工作分解結構)
|
||||||
|
|
||||||
|
- **必要內容**:可獨立交付的工作包;每個工作包的目標、範圍邊界、待辦與逐項驗收;工作包之間的先決與阻擋關係;交付文件工作包優先於純程式碼工作包,但不得違反先決關係。
|
||||||
|
- **產出位置**:工作包議題的 `待辦` 與巢狀 `驗收`,以及 `整體驗收`、`repo 列表`、`關聯` 段落;不把 WBS 寫入目標專案。
|
||||||
|
- **ELI5 變體**:把每個工作包說成一個能交付的箱子,說清楚箱子裡有什麼、完成的判準,以及哪個箱子要先完成;不得只列職責或模糊階段名稱。
|
||||||
|
|
||||||
|
### 3. 流程圖
|
||||||
|
|
||||||
|
- **必要內容**:起點、終點、主要步驟、分支條件、例外路徑與步驟間的方向;節點與邊都要能從需求或工作包驗證;超過可讀範圍時拆圖或改用文字。
|
||||||
|
- **產出位置**:交付文件預覽或使用者確認的文件位置;需求議題只保留抽象節點與邊的文字描述,不產生 HTML、SVG、附件或平台 preview。
|
||||||
|
- **ELI5 變體**:用「先做什麼、接著看什麼、遇到哪種情況走哪條路」描述;保留失敗與回復路徑,圖表視覺化時不嵌入 Mermaid 原碼。
|
||||||
|
|
||||||
|
### 4. 甘特圖
|
||||||
|
|
||||||
|
- **必要內容**:每個工作包或任務、開始日、截止日、工作日數、先決關係、交付里程碑與目前可辨識的重疊;日期必須能由排程資料重算。
|
||||||
|
- **產出位置**:排程/交付文件預覽與終端摘要;Gitea 工作包議題只保留可追蹤的截止日、里程碑與關聯,不把圖表檔寫入目標專案。
|
||||||
|
- **ELI5 變體**:把它說成一張「每件事什麼時候開始、什麼時候完成、誰要等誰」的日曆;不以顏色或位置暗示未列出的依賴。
|
||||||
|
|
||||||
|
### 5. PERT 圖
|
||||||
|
|
||||||
|
- **必要內容**:任務節點、先決關係、樂觀時間(O)、最可能時間(M)、悲觀時間(P)、期望時間與不確定性;三點估算要逐項向使用者確認,不能默認成單一工期。
|
||||||
|
- **產出位置**:排程/交付文件預覽與終端摘要;O/M/P 與推導結果是排程資料,不寫入目標專案 repo。
|
||||||
|
- **ELI5 變體**:把 O/M/P 說成最快、通常、最慢三種情況,指出哪一段最不確定;不得只報一個看似精確的日期而隱藏風險。
|
||||||
|
|
||||||
|
### 6. 關鍵路徑圖
|
||||||
|
|
||||||
|
- **必要內容**:完整相依網路、每個節點的工期、最長路徑、路徑總工期、關鍵任務與可用浮時;若有多條同長路徑要全部列出;相依成環要先報錯。
|
||||||
|
- **產出位置**:排程/交付文件預覽與終端摘要;工作包議題保留相依關係與截止日作為可重算來源,不把圖表檔寫入目標專案。
|
||||||
|
- **ELI5 變體**:說明「哪一串事情任何一件延遲都會讓最後交付延遲」,同時列出不在關鍵路徑上的緩衝;不得把所有工作都稱為關鍵。
|
||||||
|
|
||||||
|
### 7. API 契約文件
|
||||||
|
|
||||||
|
- **必要內容**:介面名稱、用途、產出者、消費者、輸入與輸出形狀、成功與錯誤情境、相容性限制、可驗收範例與來源;只記錄已確認的契約,未知內容列為未決事項。
|
||||||
|
- **產出位置**:預覽或使用者確認的交付文件位置,以及可由需求議題、工作包與實作重新產生的摘要;**禁止把 API 契約文件寫入目標專案 repo**,也不得自動修改目標專案的設定檔或文件。
|
||||||
|
- **ELI5 變體**:把 API 說成「誰用什麼資料提出請求,會拿到什麼回覆,出錯時會收到什麼」;保留欄位名稱、資料型別、錯誤碼與相容性限制,不用白話改寫掉可執行的契約。
|
||||||
|
|
||||||
|
## 確認與重產
|
||||||
|
|
||||||
|
- 每份文件產出前都先展示該類型的必要內容骨架,逐一確認內容是否齊全;使用者拒絕或未確認時不把它標成已交付。
|
||||||
|
- 文件摘要必須保留來源議題、工作包、相依與排程資料的指向。來源更新後,摘要可由同一份來源重新產生,不以手工複製的摘要作為唯一真相。
|
||||||
|
- 預覽失效、不可分享或環境不具備預覽能力時,回報實際能力與限制並逐題詢問交付方式;不得回退成寫入目標專案 repo,尤其是 API 契約文件。
|
||||||
@@ -61,7 +61,7 @@ main(async () => {
|
|||||||
目標: listSection(sections, '目標'),
|
目標: listSection(sections, '目標'),
|
||||||
非目標: listSection(sections, '非目標'),
|
非目標: listSection(sections, '非目標'),
|
||||||
名詞表: tableSection(sections, '領域名詞表'),
|
名詞表: tableSection(sections, '領域名詞表'),
|
||||||
流程圖: textSection(sections, '流程圖'),
|
文件: textSection(sections, '文件'),
|
||||||
驗收標準: listSection(sections, '驗收標準'),
|
驗收標準: listSection(sections, '驗收標準'),
|
||||||
影響範圍: listSection(sections, '影響範圍'),
|
影響範圍: listSection(sections, '影響範圍'),
|
||||||
未決事項: listSection(sections, '未決事項'),
|
未決事項: listSection(sections, '未決事項'),
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
{{名詞表}}
|
{{名詞表}}
|
||||||
|
|
||||||
## 流程圖
|
## 文件
|
||||||
|
|
||||||
{{流程圖}}
|
{{文件}}
|
||||||
|
|
||||||
## 驗收標準
|
## 驗收標準
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readReference, readTemplate } from './helpers/prompt-doc.js';
|
||||||
|
|
||||||
|
const rules = readReference('delivery-types');
|
||||||
|
const template = readTemplate('requirement-issue');
|
||||||
|
const TYPES = [
|
||||||
|
'需求描述概要',
|
||||||
|
'WBS(工作分解結構)',
|
||||||
|
'流程圖',
|
||||||
|
'甘特圖',
|
||||||
|
'PERT 圖',
|
||||||
|
'關鍵路徑圖',
|
||||||
|
'API 契約文件',
|
||||||
|
];
|
||||||
|
|
||||||
|
function sectionOf(text, heading) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const start = lines.findIndex((line) => line.replace(/^### (?:\d+\. )?/, '') === heading);
|
||||||
|
assert.notEqual(start, -1, `規則正本缺少「${heading}」`);
|
||||||
|
const rest = lines.slice(start + 1);
|
||||||
|
const end = rest.findIndex((line) => line.startsWith('### '));
|
||||||
|
return (end === -1 ? rest : rest.slice(0, end)).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('需求模板以文件取代流程圖,且保留固定段落順序', () => {
|
||||||
|
const headings = [...template.matchAll(/^## (.+)$/gm)].map((match) => match[1]);
|
||||||
|
assert.deepEqual(headings, [
|
||||||
|
'總覽', '背景', '目標', '非目標', '領域名詞表', '文件', '驗收標準', '影響範圍', '未決事項',
|
||||||
|
]);
|
||||||
|
assert.match(template, /\{\{文件\}\}/);
|
||||||
|
assert.equal(template.includes('## 流程圖'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('規則正本逐一交代七種文件的必要內容、產出位置與 ELI5 變體', () => {
|
||||||
|
assert.match(rules, /^## 七種交付類型$/m);
|
||||||
|
for (const type of TYPES) {
|
||||||
|
const section = sectionOf(rules, type);
|
||||||
|
assert.match(section, /必要內容/);
|
||||||
|
assert.match(section, /產出位置/);
|
||||||
|
assert.match(section, /ELI5 變體/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('API 契約規則禁止寫入目標專案,且摘要可由來源重產', () => {
|
||||||
|
const section = sectionOf(rules, 'API 契約文件');
|
||||||
|
assert.match(section, /禁止把 API 契約文件寫入目標專案 repo/);
|
||||||
|
assert.match(rules, /摘要可由同一份來源重新產生/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('交付文件共通規則保留逐一確認與預覽能力分流', () => {
|
||||||
|
assert.match(rules, /逐一確認/);
|
||||||
|
assert.match(rules, /可開啟、可分享的預覽/);
|
||||||
|
assert.match(rules, /沒有預覽能力時依使用者確認的方式交付/);
|
||||||
|
});
|
||||||
+11
-11
@@ -40,7 +40,7 @@ const FULL_BODY = `## 總覽
|
|||||||
| 需求議題 | 描述一項需求的 Gitea issue |
|
| 需求議題 | 描述一項需求的 Gitea issue |
|
||||||
| 工作包 | 從需求拆出的可獨立完成的單位 |
|
| 工作包 | 從需求拆出的可獨立完成的單位 |
|
||||||
|
|
||||||
## 流程圖
|
## 文件
|
||||||
|
|
||||||
\`\`\`mermaid
|
\`\`\`mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -108,7 +108,7 @@ test('抽出契約上的每一個欄位', async (t) => {
|
|||||||
assert.equal(code, 0);
|
assert.equal(code, 0);
|
||||||
assert.deepEqual(Object.keys(json.data).sort(), [
|
assert.deepEqual(Object.keys(json.data).sort(), [
|
||||||
'index', 'labels', 'title', 'url',
|
'index', 'labels', 'title', 'url',
|
||||||
'影響範圍', '未決事項', '未處理留言數', '流程圖',
|
'影響範圍', '未決事項', '未處理留言數', '文件',
|
||||||
'目標', '總覽', '背景', '名詞表', '非目標', '驗收標準',
|
'目標', '總覽', '背景', '名詞表', '非目標', '驗收標準',
|
||||||
].sort());
|
].sort());
|
||||||
});
|
});
|
||||||
@@ -159,14 +159,14 @@ test('名詞表解析成 term 與 def,表頭與分隔列不算一筆', async (
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('流程圖原樣帶出,連圍欄一起', async (t) => {
|
test('文件原樣帶出,連圍欄一起', async (t) => {
|
||||||
const stub = await withStub(t);
|
const stub = await withStub(t);
|
||||||
|
|
||||||
const { json } = await run([], stub);
|
const { json } = await run([], stub);
|
||||||
|
|
||||||
assert.match(json.data.流程圖, /^```mermaid/);
|
assert.match(json.data.文件, /^```mermaid/);
|
||||||
assert.match(json.data.流程圖, /flowchart TD/);
|
assert.match(json.data.文件, /flowchart TD/);
|
||||||
assert.match(json.data.流程圖, /```$/);
|
assert.match(json.data.文件, /```$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 模板變體 ───────────────────────────────────────────────────────
|
// ── 模板變體 ───────────────────────────────────────────────────────
|
||||||
@@ -192,7 +192,7 @@ test('缺段落回傳空值而不是報錯', async (t) => {
|
|||||||
assert.equal(code, 0);
|
assert.equal(code, 0);
|
||||||
assert.equal(json.data.總覽, '只有總覽的議題。');
|
assert.equal(json.data.總覽, '只有總覽的議題。');
|
||||||
assert.equal(json.data.背景, '');
|
assert.equal(json.data.背景, '');
|
||||||
assert.equal(json.data.流程圖, '');
|
assert.equal(json.data.文件, '');
|
||||||
assert.deepEqual(json.data.目標, []);
|
assert.deepEqual(json.data.目標, []);
|
||||||
assert.deepEqual(json.data.名詞表, []);
|
assert.deepEqual(json.data.名詞表, []);
|
||||||
assert.deepEqual(json.data.驗收標準, []);
|
assert.deepEqual(json.data.驗收標準, []);
|
||||||
@@ -354,13 +354,13 @@ test('--dry-run 印出將發出的請求,且不碰 Gitea', async (t) => {
|
|||||||
// ── 圍欄與表格的邊界(皆為 code review 抓出的實際缺陷,這裡封住回頭路)────
|
// ── 圍欄與表格的邊界(皆為 code review 抓出的實際缺陷,這裡封住回頭路)────
|
||||||
|
|
||||||
test('~~~ 圍欄裡的井字號不是標題', async (t) => {
|
test('~~~ 圍欄裡的井字號不是標題', async (t) => {
|
||||||
const body = '## 流程圖\n\n~~~\n## 這不是標題\n~~~\n\n## 目標\n\n- 真的目標\n';
|
const body = '## 文件\n\n~~~\n## 這不是標題\n~~~\n\n## 目標\n\n- 真的目標\n';
|
||||||
const stub = await withStub(t, {}, { body });
|
const stub = await withStub(t, {}, { body });
|
||||||
|
|
||||||
const { json } = await run([], stub);
|
const { json } = await run([], stub);
|
||||||
|
|
||||||
assert.deepEqual(json.data.目標, ['真的目標']);
|
assert.deepEqual(json.data.目標, ['真的目標']);
|
||||||
assert.match(json.data.流程圖, /這不是標題/, '圍欄內容原樣留在流程圖段落裡');
|
assert.match(json.data.文件, /這不是標題/, '圍欄內容原樣留在文件段落裡');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('圍欄裡的減號不是清單項', async (t) => {
|
test('圍欄裡的減號不是清單項', async (t) => {
|
||||||
@@ -382,14 +382,14 @@ test('圍欄要同種標記才算關閉,混用時不會提早收尾', async (t
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('圍欄沒關就到結尾時,其後內容算在圍欄內(與 markdown 渲染一致)', async (t) => {
|
test('圍欄沒關就到結尾時,其後內容算在圍欄內(與 markdown 渲染一致)', async (t) => {
|
||||||
const body = '## 流程圖\n\n```mermaid\nflowchart TD\n\n## 驗收標準\n\n- 被圍欄吃掉\n';
|
const body = '## 文件\n\n```mermaid\nflowchart TD\n\n## 驗收標準\n\n- 被圍欄吃掉\n';
|
||||||
const stub = await withStub(t, {}, { body });
|
const stub = await withStub(t, {}, { body });
|
||||||
|
|
||||||
const { code, json } = await run([], stub);
|
const { code, json } = await run([], stub);
|
||||||
|
|
||||||
assert.equal(code, 0, '不該炸掉,只是內容歸屬不同');
|
assert.equal(code, 0, '不該炸掉,只是內容歸屬不同');
|
||||||
assert.deepEqual(json.data.驗收標準, []);
|
assert.deepEqual(json.data.驗收標準, []);
|
||||||
assert.match(json.data.流程圖, /驗收標準/);
|
assert.match(json.data.文件, /驗收標準/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('名詞表的欄位內容可以有逸脫的直線', async (t) => {
|
test('名詞表的欄位內容可以有逸脫的直線', async (t) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user