ci: diff mutants only on request; the review pipeline proves them on the material before reviewing

Validate ran the three "Мутанты по диффу" shards on every push of every
branch: 48 of 56 job-hours on 08–09.09, most of them cancelled by the
next push. Mutants now run when asked — pull requests, the nightly
schedule, a push carrying a `Release:` trailer, or a dispatch with
`mutants=true` (classify-changes.mjs → `mutants_requested`); an ordinary
push runs the light checks only.

The proof moves to where it is consumed. process.yml gets a gate after
the #499 reuse step: on the code stage it looks for a dispatch Validate
run on the exact material SHA whose mutant jobs executed and passed
(scripts/validate-gate.mjs); none → it dispatches one and waits; red or
missing → the task goes back S7→S6 with the run link and the review
cycle is not spent. Spec stage and the reuse fast-path skip the gate
(`proceed=true`); all later steps branch on `proceed` in place of the
old conflict conjunct only. merge-candidate.mjs dispatches Validate on
the pushed candidate and waits for that dispatch run.

PROCESS.md/AGENTS.md: review does not start on red code; one handoff —
one push.

Mutants: mutants-run-on-every-push, review-starts-on-red-validate,
review-trusts-push-run-without-mutants, merge-waits-push-run-without-mutants.

Issue: #510
User-Visible: no
This commit is contained in:
Codex
2026-09-09 15:14:20 +00:00
committed by claude[bot]
parent 34c5c2d134
commit 97dfa457a4
14 changed files with 504 additions and 34 deletions
+9 -2
View File
@@ -366,14 +366,21 @@ test('релизные гейты требуют трейлер Release: и св
assert.match(read('publish-prerelease.yml'), /check-docs\.mjs --screenshots=strict/);
});
test('мутанты по диффу гоняются на каждом пуше с базы диапазона (#475 AC4)', () => {
test('мутанты по диффу гоняются по запросу с базы диапазона (#475 AC4, #510 AC1)', () => {
const workflow = read('validate.yml');
const start = workflow.indexOf('\n changed_mutants:\n');
assert.ok(start > 0, 'нет job changed_mutants');
const job = workflow.slice(start, workflow.indexOf('\n frontend:\n', start));
// Триггер — и фронтенд, и бэкенд: бэкенд-мутанты патчат .py и охраняются
// pytest, а дифф только по ним даёт backend=true без frontend=true (ревью r1).
assert.match(job, /if: needs\.changes\.outputs\.frontend == 'true' \|\| needs\.changes\.outputs\.backend == 'true' \|\| needs\.changes\.outputs\.mutants == 'true'/);
// #510: job идёт ровно тогда, когда мутанты запрошены — dispatch mutants/full,
// PR, ночь, кандидат беты; обычный push обходится дешёвыми гейтами. Отбор по
// файлам живёт внутри job (`--changed`): при запросе она обязана исполниться,
// иначе гейт ревью не отличит «нечего гонять» от «не запрашивали» (ревью ТЗ r1).
assert.match(job, /\n if: needs\.changes\.outputs\.mutants_requested == 'true'\n/);
assert.match(workflow, /mutants_requested: \$\{\{ steps\.heavy\.outputs\.mutants_requested \}\}/);
assert.match(workflow, /MUTANTS_INPUT: \$\{\{ inputs\.mutants \}\}/);
assert.match(workflow, /\n mutants:\n description: [^\n]*\n type: boolean\n default: false\n/, 'вход workflow_dispatch mutants, по умолчанию выключен');
// Третий дизъюнкт (ТЗ §2, ревью r1): правка одного реестра мутантов — тоже
// вход гейта, классификатор обязан выдавать `mutants` по этому файлу.
assert.match(workflow, /mutants: \$\{\{ steps\.classify\.outputs\.mutants \}\}/);