fix(process): the #562 infra entry no longer covers an issue in S3/S4 (#753)

Rule 8 skipped the status check for any range without class A files, so a
task in S3-spec or S4-spec-review could push a branch of tests, demo or
scripts with only a warning. §11.8 forbids exactly that: before S5 neither
class A commits nor the branch itself is pushed, because the spec-review
step takes the freshest origin/issue/<NN>-* as its material and lays the
SPEC-REVIEW document there, putting code in front of a spec reviewer who
must not read it (§2.4).

The #562 entry was written for a task before its first S status. The
decision is now made per issue in checkIssueStatuses: the status stays
optional only when the range is infrastructural and the issue carries
neither S3-spec nor S4-spec-review. Such an issue gets a rule 8 refusal
naming its status and §11.8; the range-wide #562 warning is still printed.
No status, S1-new/S2-analysis (reviewer-filed infra issues) and S5-S8 keep
their old outcome; closed, blocked and fail-closed checks are untouched;
rule 10 is still called only for ranges with class A.

PROCESS.md §10.2 gets the one-sentence exception, the mutation registry a
mutant that drops the S3/S4 condition.

Issue: #753
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 14:03:19 +00:00
committed by claude[bot]
parent 9deed2a3c3
commit 4aa6c85f98
4 changed files with 141 additions and 3 deletions
+18 -2
View File
@@ -411,6 +411,12 @@ export function commitsUnderRuleOne(commits) {
// поставить продуктовой задаче и увести продуктовый коммит от проверки статуса,
// а перестать трогать класс A, не перестав быть инфраструктурной задачей,
// нельзя. Существование issue, его открытость и `blocked` проверяются всё равно.
//
// Признак диапазона не знает, где задача. Задача в маршруте ТЗ (`S3-spec`,
// `S4-spec-review`: `track:ask`, повышенный show после `reclassify` #726, ручной
// возврат) уже не «до первого S-статуса»: её ветка до `S5` не пушится вовсе
// (§11.8), иначе ревью ТЗ получает её материалом. Поэтому исключение снимается
// по каждому issue в `checkIssueStatuses`, а не по диапазону (#753).
export function isInfrastructureRange(commits) {
if (!commits.length) return false;
return !commits.some((c) => c.classes.has('A'));
@@ -450,6 +456,12 @@ export function commitsNeedingIssueStatus(
return underRuleOne.filter((commit) => !isPublishedPrereleaseCommit(commit.sha));
}
// Статусы маршрута ТЗ, для которых инфраструктурный вход #562 не действует
// (#753). `S1-new` и `S2-analysis` в него не входят: инфраструктурные issue,
// заведённые ревьюером, приходят с `S1-new`, а ветка до `S4` материалом ревью
// ТЗ не становится.
export const SPEC_ROUTE_STATUS = ['S3-spec', 'S4-spec-review'];
// 8. статус issue. Fail closed: недоступный или закрытый issue — отказ, а не
// пропуск. Гейт, который молчит при недоступном источнике правды, бесполезен.
export function checkIssueStatuses(
@@ -484,11 +496,15 @@ export function checkIssueStatuses(
continue;
}
const names = (issue.labels ?? []).map((l) => (typeof l === 'string' ? l : l.name));
if (!statusOptional && !names.some((n) => allowed.includes(n))) {
const specRoute = names.filter((n) => SPEC_ROUTE_STATUS.includes(n));
const optional = statusOptional && specRoute.length === 0;
if (!optional && !names.some((n) => allowed.includes(n))) {
const status = names.filter((n) => /^S\d-/.test(n));
out.push({
level: 'fail', rule: 8, sha: '-',
msg: `issue #${nn}: статус ${status.length ? status.join(',') : 'не проставлен'}, а нужен один из ${allowed.join(' / ')}`,
msg: statusOptional
? `issue #${nn} в ${specRoute.join(',')}: ветка задачи до «Готово к разработке» не пушится (§11.8); инфраструктурный вход (#562) — не для задачи в маршруте ТЗ`
: `issue #${nn}: статус ${status.length ? status.join(',') : 'не проставлен'}, а нужен один из ${allowed.join(' / ')}`,
});
}
if (names.includes('blocked')) {