/** * 正本第一段「領取與開工準備」的規則。 * * 這些檔案是文件不是程式,但它們是指令實際交付的東西:決策表寫錯,被鎖擋下來的人 * 就會得到錯的下一步;邊界寫漏,第一段就會去做後面幾段的事。靠人記不牢,用測試釘住。 */ 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 phase1 = prompt.slice(prompt.indexOf('## 第一段'), prompt.indexOf('## 邊界')); test('正本平台中立,description 前綴正確', () => { assertNeutralPrompt(prompt, 'sdlc-feat'); }); test('第一段指名三支腳本,順序為先讀再領再備分支', () => { const order = ['wp-extract.js', 'claim.js', 'branch-prep.js']; const positions = order.map((name) => phase1.indexOf(name)); assert.equal(positions.every((p) => p >= 0), true, '三支腳本都要被指名'); assert.deepEqual([...positions].sort((a, b) => a - b), positions, '領取之前要先讀得懂這顆在做什麼'); }); test('未處理留言不是 0 時要先停下來提示整併', () => { assert.match(phase1, /未處理留言數/); assert.match(phase1, /先停下來/); assert.match(phase1, /sdlc-sync/); }); test('領取鎖的四種狀態各自交代了下一步,含放行那一種', () => { for (const code of ['CLAIMED_BY_OTHER', 'STOPWATCH_ON_THIS_ISSUE', 'STOPWATCH_ON_OTHER_ISSUE']) { assert.match(phase1, new RegExp(code), `${code} 要出現在決策表裡`); } assert.match(phase1, /沒有鎖/, '第四種狀態(放行)也要在表上,否則只剩擋的那幾種'); assert.match(phase1, /不要繞過去/, '被擋下來的處置要明講,不能靠 agent 自由發揮'); }); test('缺標籤是前置條件,不混進領取鎖的四種狀態裡', () => { const table = phase1.slice(phase1.indexOf('| 狀態'), phase1.indexOf('碼錶一律由使用者自己停')); assert.equal(table.includes('LABEL_NOT_FOUND'), false, '它不是鎖的狀態,別讓四種變五種'); assert.match(phase1, /LABEL_NOT_FOUND/, '但仍要交代它,否則使用者不知道怎麼辦'); }); test('工作包跨多個 repo 時怎麼開分支,有交代', () => { assert.match(phase1, /repos/); assert.match(phase1, /有多顆時逐一確認/); }); test('工作區不乾淨時的處置寫明了,且不替使用者決定', () => { assert.match(phase1, /DIRTY_WORKTREE/); assert.match(phase1, /不要自己選/); }); test('碼錶只由使用者自己停,並說明為什麼不代勞', () => { assert.match(phase1, /由使用者自己停/); assert.match(phase1, /工時記錯地方/); }); test('來源分支要問過使用者,且一次一題、附理由與手動輸入', () => { assert.match(phase1, /一次問一題/); assert.match(phase1, /手動輸入/); assert.match(phase1, /不要替他決定|不要替使用者決定/); }); test('翻譯規則釘住 kebab 與 40 字元上限,並舉出可照抄的例子', () => { assert.match(phase1, /kebab/); assert.match(phase1, /40/); assert.match(phase1, /wp-extract-contract/, '要有一個真的例子,不要只說規則'); assert.match(phase1, /不要把長句截斷/); }); test('--type 什麼時候要給、什麼時候不能給,寫清楚了', () => { assert.match(phase1, /`--type` 只在來源是開發分支時要給/); assert.match(phase1, /沿用來源/); }); test('兩處「不覆蓋他人進度」的保證都有寫出來', () => { assert.match(phase1, /pull 而不是重建/); assert.match(phase1, /接上去而不是蓋掉/); }); test('三支腳本的寫入都要求先試跑', () => { const dryRuns = phase1.match(/--dry-run/g) ?? []; assert.ok(dryRuns.length >= 2, `兩支寫入型腳本各要先試跑,只找到 ${dryRuns.length} 處`); }); test('邊界把第一段不做的事分開列,且明講不寫本機狀態檔', () => { const boundary = prompt.slice(prompt.indexOf('## 邊界')); assert.match(boundary, /不改任何一行程式碼/); assert.match(boundary, /不勾待辦/); assert.match(boundary, /不開 PR/); assert.match(boundary, /不自行建立標籤/); assert.match(boundary, /不寫任何本機狀態檔/); assert.match(boundary, /換一台機器或換一個 agent/, '要說明為什麼不留狀態檔'); });