|
| 1 | +import fs from 'fs'; |
| 2 | +import path from 'path'; |
| 3 | +import { spawnSync } from 'child_process'; |
| 4 | + |
| 5 | +const ROOT = process.cwd(); |
| 6 | +const ADR_DIR = path.join(ROOT, 'docs', 'adr'); |
| 7 | +const DOCS_INDEX = path.join(ROOT, 'docs', 'README.md'); |
| 8 | +const ADR_INDEX = path.join(ADR_DIR, 'README.md'); |
| 9 | +const AGENTS = path.join(ROOT, 'AGENTS.md'); |
| 10 | + |
| 11 | +const adrTriggerRules = [ |
| 12 | + '선택지가 2개 이상이고 트레이드오프가 존재한 경우.', |
| 13 | + '반복적으로 따라야 할 규칙이나 경계를 정의한 경우.', |
| 14 | + '테스트 전략이나 검증 방식이 결정의 핵심이었던 경우.', |
| 15 | +]; |
| 16 | + |
| 17 | +const markdownlintTargets = [ |
| 18 | + 'AGENTS.md', |
| 19 | + 'ARCHITECTURE.md', |
| 20 | + 'docs/README.md', |
| 21 | + ...fs |
| 22 | + .readdirSync(ADR_DIR) |
| 23 | + .filter((fileName) => fileName.endsWith('.md')) |
| 24 | + .sort() |
| 25 | + .map((fileName) => path.join('docs', 'adr', fileName)), |
| 26 | +]; |
| 27 | + |
| 28 | +function readText(filePath) { |
| 29 | + return fs.readFileSync(filePath, 'utf8'); |
| 30 | +} |
| 31 | + |
| 32 | +function assertCondition(condition, message) { |
| 33 | + if (!condition) { |
| 34 | + throw new Error(message); |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +function assertFileExists(relativePath) { |
| 39 | + assertCondition( |
| 40 | + fs.existsSync(path.join(ROOT, relativePath)), |
| 41 | + `Missing docs index target: ${relativePath}` |
| 42 | + ); |
| 43 | +} |
| 44 | + |
| 45 | +function verifyDocsIndexTargets() { |
| 46 | + const docsIndex = readText(DOCS_INDEX); |
| 47 | + const linkedPaths = [...docsIndex.matchAll(/`([^`]+)`/g)] |
| 48 | + .map((match) => match[1]) |
| 49 | + .filter((entry) => !entry.includes('*')) |
| 50 | + .filter((entry) => entry.endsWith('.md') || entry.endsWith('.sql')); |
| 51 | + |
| 52 | + linkedPaths.forEach(assertFileExists); |
| 53 | +} |
| 54 | + |
| 55 | +function verifyAdrTriggerRules() { |
| 56 | + const agents = readText(AGENTS); |
| 57 | + const adrIndex = readText(ADR_INDEX); |
| 58 | + |
| 59 | + adrTriggerRules.forEach((rule) => { |
| 60 | + assertCondition( |
| 61 | + agents.includes(rule), |
| 62 | + `AGENTS.md is missing ADR trigger rule: ${rule}` |
| 63 | + ); |
| 64 | + assertCondition( |
| 65 | + adrIndex.includes(rule), |
| 66 | + `docs/adr/README.md is missing ADR trigger rule: ${rule}` |
| 67 | + ); |
| 68 | + }); |
| 69 | +} |
| 70 | + |
| 71 | +function verifyAdrIndexEntries() { |
| 72 | + const adrIndex = readText(ADR_INDEX); |
| 73 | + const adrFiles = fs |
| 74 | + .readdirSync(ADR_DIR) |
| 75 | + .filter((fileName) => /^\d{4}-.+\.md$/.test(fileName)) |
| 76 | + .sort(); |
| 77 | + |
| 78 | + adrFiles.forEach((fileName) => { |
| 79 | + assertCondition( |
| 80 | + adrIndex.includes(`](${fileName})`), |
| 81 | + `docs/adr/README.md does not list ${fileName}` |
| 82 | + ); |
| 83 | + }); |
| 84 | +} |
| 85 | + |
| 86 | +function runMarkdownlint() { |
| 87 | + const result = spawnSync( |
| 88 | + path.join(ROOT, 'node_modules', '.bin', 'markdownlint-cli2'), |
| 89 | + [ |
| 90 | + '--config', |
| 91 | + path.join('internal', 'config', '.markdownlint-cli2.jsonc'), |
| 92 | + ...markdownlintTargets, |
| 93 | + ], |
| 94 | + { |
| 95 | + cwd: ROOT, |
| 96 | + stdio: 'inherit', |
| 97 | + } |
| 98 | + ); |
| 99 | + |
| 100 | + assertCondition( |
| 101 | + result.status === 0, |
| 102 | + `markdownlint failed with exit code ${result.status ?? 'unknown'}` |
| 103 | + ); |
| 104 | +} |
| 105 | + |
| 106 | +function main() { |
| 107 | + verifyDocsIndexTargets(); |
| 108 | + verifyAdrTriggerRules(); |
| 109 | + verifyAdrIndexEntries(); |
| 110 | + runMarkdownlint(); |
| 111 | + console.log('Documentation verification passed.'); |
| 112 | +} |
| 113 | + |
| 114 | +try { |
| 115 | + main(); |
| 116 | +} catch (error) { |
| 117 | + console.error( |
| 118 | + error instanceof Error ? error.message : 'Documentation verification failed.' |
| 119 | + ); |
| 120 | + process.exit(1); |
| 121 | +} |
0 commit comments