/** * 實作規範與註解格式對照表這兩份規則正本。 * * 它們是 /sdlc-feat 第二段實際交付的東西:規範寫漏一條,產出的程式碼就少一種註解, * 而那要等 reviewer 看到才會發現。對照表少一種語言,agent 就會開始猜格式。 */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readReference } from './helpers/prompt-doc.js'; const standards = readReference('coding-standards'); const styles = readReference('comment-styles'); /** * 切出一個 `## 標題` 段落。 * 以整行比對而不是 indexOf:`## Java` 是 `## JavaScript` 的前綴, * 用 indexOf 會切到錯的那一節,而且切出來還是有內容的,錯得很安靜。 */ function sectionOf(doc, heading) { const lines = doc.split('\n'); const start = lines.findIndex((line) => line.trim() === `## ${heading}`); if (start === -1) return null; const rest = lines.slice(start + 1); const end = rest.findIndex((line) => line.startsWith('## ')); return (end === -1 ? rest : rest.slice(0, end)).join('\n'); } // ── 實作規範 ─────────────────────────────────────────────────────── test('六種專案檔都對得到語言', () => { for (const file of [ '\\*\\.csproj', 'composer\\.json', 'package\\.json', 'go\\.mod', 'pom\\.xml', 'pyproject\\.toml', ]) { assert.match(standards, new RegExp(file), `專案檔對照缺少 ${file}`); } }); test('認不出語言時要停下來問,而且說明了為什麼不猜', () => { assert.match(standards, /認不出來就停下來問/); assert.match(standards, /不要猜/); assert.match(standards, /比沒有註解更難清理/, '要說出猜錯的代價,否則這條規則會被當成客套話'); }); test('分層判定明講看職責不看目錄', () => { assert.match(standards, /看職責,不看目錄/); assert.match(standards, /目錄名稱會騙人/); }); test('三層各自要寫哪一種註解都寫明了', () => { for (const [layer, comment] of [ ['控制層', '功能註解'], ['服務層', '邏輯註解'], ['存取層', '資料源註解'], ]) { const row = standards.split('\n').find((line) => line.includes(layer) && line.includes('|')); assert.ok(row, `${layer}沒有出現在分層表裡`); assert.match(row, new RegExp(comment), `${layer}要寫的是${comment}`); } }); test('服務層要標註呼叫的方法,並說明理由是追呼叫鏈', () => { assert.match(standards, /標註它呼叫的所有方法/); assert.match(standards, /追得到呼叫鏈/); }); test('屬性註解要遞迴,而且明講不能只註解最外層', () => { assert.match(standards, /屬性本身是類別時遞迴處理/); assert.match(standards, /不能只註解最外層/); }); test('資料範例的來源有優先序,且未經驗證時要註明', () => { assert.match(standards, /優先從 MCP 取得/); assert.match(standards, /由邏輯推理、未經驗證/); assert.match(standards, /有人會照著那個格式寫解析/, '要說出不註明的代價'); }); test('明講不寫入目標專案的任何檔案', () => { assert.match(standards, /不寫入目標專案的任何檔案/); assert.match(standards, /CLAUDE\.md/); }); // ── 註解格式對照表 ───────────────────────────────────────────────── test('六種語言各有一節,且都附可照抄的程式碼範例', () => { for (const [language, marker] of [ ['C#', '///'], ['PHP', '@var'], ['JavaScript/TypeScript', 'JSDoc'], ['Go', 'go doc'], ['Java', 'Javadoc'], ['Python', 'docstring'], ]) { const body = sectionOf(styles, language); assert.ok(body, `對照表缺少 ${language}`); assert.match(body, new RegExp(marker.replace(/[/#]/g, '\\$&')), `${language} 缺少 ${marker}`); assert.match(body, /```/, `${language} 要有可照抄的範例,不要只用文字描述`); } }); test('每個語言的範例都同時示範了方法註解與屬性註解', () => { const sections = styles.split(/^## /m).filter((s) => s.includes('```')); for (const section of sections) { const name = section.split('\n')[0].trim(); if (name === '未經驗證的範例怎麼標') continue; assert.match(section, /例:|例如/, `${name} 的範例要示範「附真實資料範例」這件事`); } }); test('Go 的慣例(以識別字開頭)有被指出來,不是照抄別的語言', () => { assert.match(sectionOf(styles, 'Go'), /以被註解的識別字開頭/); }); test('Python 的 docstring 位置有講清楚在定義的下一行', () => { const python = sectionOf(styles, 'Python'); assert.match(python, /下一行/); assert.match(python, /不是上一行/, '這是最容易寫錯的一點,要明講'); }); test('未經驗證的註明怎麼寫,兩種語言各有一個可照抄的寫法', () => { const section = sectionOf(styles, '未經驗證的範例怎麼標'); assert.match(section, /不要另起一行 TODO/); assert.ok((section.match(/由邏輯推理、未經驗證/g) ?? []).length >= 2, '至少要有兩種語言的寫法'); }); // ── 兩份的分工 ───────────────────────────────────────────────────── test('規範與格式分開:對照表不重複寫一遍規範', () => { assert.match(styles, /這份只管\*\*格式\*\*/); assert.match(standards, /comment-styles\.md/, '規範要指名去哪裡查格式'); });