diff --git a/scripts/validate-changelog-frontmatter.mjs b/scripts/validate-changelog-frontmatter.mjs index 74cbb9ae..54bf11cc 100644 --- a/scripts/validate-changelog-frontmatter.mjs +++ b/scripts/validate-changelog-frontmatter.mjs @@ -447,8 +447,51 @@ export function runFrontmatterValidation(records, root = process.cwd()) { return { checkedCount: documents.length, errors }; } +/** + * 提交前自检模式:`--files a.md b.md`(或逗号分隔)按「新增文件」口径校验工作区里的 changelog, + * 让作者/Agent 不用先 commit 就能跑到与 pre-push 门禁完全相同的规则(hl-workflow changelog_workflow.py lint 也走这里)。 + */ +export function runFilesValidation(files, root = process.cwd()) { + const errors = []; + let checkedCount = 0; + for (const raw of files) { + const absolute = path.resolve(root, raw); + const relative = path.relative(root, absolute).split(path.sep).join('/'); + if (!controlledRootForPath(relative) || !relative.endsWith('.md')) { + errors.push(ruleError('E_PATH', relative, '不在受控 changelog 目录内(changelogs/ 或 changelogs-v2/)')); + continue; + } + let text; + try { + text = readFileSync(absolute, 'utf8'); + } catch (error) { + errors.push(ruleError('E_READ', relative, `无法读取文件: ${error.message}`)); + continue; + } + checkedCount += 1; + errors.push(...validateV2Document(relative, text, { requireV2: true })); + } + return { checkedCount, errors }; +} + export function main(argv = process.argv.slice(2)) { try { + if (argv[0] === '--files') { + const files = argv.slice(1).flatMap((item) => item.split(',')).filter(Boolean); + if (files.length === 0) { + throw new Error('--files 需要至少一个文件路径'); + } + const result = runFilesValidation(files); + if (result.errors.length > 0) { + for (const error of result.errors) { + console.error(`[${error.code}] ${error.path}: ${error.message}`); + } + console.error(`FAIL: ${result.errors.length} frontmatter error(s) in ${result.checkedCount} changelog file(s).`); + return 1; + } + console.log(`PASS: validated frontmatter for ${result.checkedCount} changelog file(s) (--files).`); + return 0; + } const options = parseArguments(argv); const records = parseNameStatusZ(diffFromOptions(options)); const result = runFrontmatterValidation(records);