From 30698d6ec454af2c5c6ec825858bbc1b9ec0700b Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 26 Aug 2026 00:24:22 +0300 Subject: [PATCH] fix: exempt pipeline review documents from the branch-name gate rule (#305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правило 2 локального process-gate блокировало пуш issue-ветки после того, как конвейер «Привести ветку к dev» вносил в неё свежую историю dev с чужим коммитом «docs: review document for #NNN» — локальный origin/dev автора отставал и не вычитал его из диапазона, а нарушение в истории не чинится следующим коммитом (единственный выход был --no-verify по §12/17). Исключение доказуемое и fail-closed: точный subject документа ревью И дифф только в docs/reviews/ (files обязателен — ownCommits теперь читаются с filesOf). Любой код рядом с документом или пустой список файлов возвращают правило 2 в строй; юниты фиксируют оба контура. Issue: #305 User-Visible: no --- scripts/process-gate.mjs | 17 +++++++++++++++++ test/process-gate.test.mjs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/scripts/process-gate.mjs b/scripts/process-gate.mjs index a987cfe9..1d6671c7 100644 --- a/scripts/process-gate.mjs +++ b/scripts/process-gate.mjs @@ -202,12 +202,26 @@ export function evaluateCommit(c) { // уезжает назад, и в диапазон попадают коммиты `dev` с чужими номерами issue — // каждый из них выглядел бы нарушением. Проверено на реальной истории: сидя на // issue/89 с диапазоном по dev, гейт дал 26 ложных отказов из 26 коммитов. +// Документ ревью, доказанно созданный конвейером (#305): точный subject +// «docs: review document for #NN» и дифф ТОЛЬКО в docs/reviews/. Такой коммит +// попадает в чужую issue-ветку легитимно — шаг «Привести ветку к dev» +// вносит свежую историю dev, где конвейер только что опубликовал документ +// соседней задачи, а локальный origin/dev автора мог отстать и не вычесть его +// из диапазона. Кода в таком коммите нет по построению (files-критерий), и +// судить им правило 2 — ложный отказ, чинимый только --no-verify. +export function isPipelineReviewDocCommit(c) { + return /^docs: review document for #\d+$/.test(c.subject ?? '') + && (c.files?.length ?? 0) > 0 + && c.files.every((f) => f.startsWith('docs/reviews/')); +} + export function checkBranchRule(branch, commits) { const m = (branch ?? '').match(/^issue\/(\d+)-/); if (!m) return []; const want = `#${m[1]}`; const out = []; for (const c of commits) { + if (isPipelineReviewDocCommit(c)) continue; for (const t of c.issues) { if (t !== want) { out.push({ @@ -541,8 +555,11 @@ function main(argv) { const hasDev = spawnSync('git', ['-C', repo, 'rev-parse', '--verify', 'origin/dev'], { encoding: 'utf8' }).status === 0; if (!hasDev) return checkedCommits; + // filesOf обязателен: исключение #305 доказывается диффом коммита + // (только docs/reviews/), а без файлов оно не срабатывает fail-closed. const own = parseRecords( git(['log', '--reverse', `--pretty=format:${LOG_FORMAT}`, 'origin/dev..HEAD'], repo), + filesOf, ); return commitsNeedingTargetValidation(own, { targetRef, isCommitOnMain }); })() diff --git a/test/process-gate.test.mjs b/test/process-gate.test.mjs index a83fac17..9117fa35 100644 --- a/test/process-gate.test.mjs +++ b/test/process-gate.test.mjs @@ -11,6 +11,7 @@ import { STRICT_STATUS, buildReport, checkBranchRule, + isPipelineReviewDocCommit, checkIssueStatuses, checkReviewDocLimit, REVIEW_DOC_LIMIT, @@ -648,3 +649,34 @@ test('the CLI waives the issue status for a class-B-only range but not with clas rmSync(dir, { recursive: true, force: true }); } }); + +test('rule 2 exempts a pipeline review document proven by subject and diff (#305)', () => { + const doc = makeCommit({ + sha: 'a'.repeat(40), + subject: 'docs: review document for #304', + body: 'Issue: #304\nUser-Visible: no', + files: ['docs/reviews/CODE-REVIEW-304-r1.md'], + }); + assert.equal(isPipelineReviewDocCommit(doc), true); + assert.deepEqual(checkBranchRule('issue/302-junction-node-material', [doc]), []); + + // Any code beside the document voids the proof — rule 2 fires again. + const forged = makeCommit({ + sha: 'b'.repeat(40), + subject: 'docs: review document for #304', + body: 'Issue: #304', + files: ['docs/reviews/CODE-REVIEW-304-r1.md', 'src/houseplan-card.ts'], + }); + assert.equal(isPipelineReviewDocCommit(forged), false); + assert.equal(checkBranchRule('issue/302-junction-node-material', [forged]).length, 1); + + // Fail-closed: without a file list the exemption cannot prove itself. + const blind = makeCommit({ + sha: 'c'.repeat(40), + subject: 'docs: review document for #304', + body: 'Issue: #304', + files: [], + }); + assert.equal(isPipelineReviewDocCommit(blind), false); + assert.equal(checkBranchRule('issue/302-junction-node-material', [blind]).length, 1); +});