process: mutants run only in the nightly full registry; speed rules for ship/show (#709)

Owner's decision 2026-09-29: mutants check the tests, not the product.
During development they are not run at all — not locally, not in CI,
not by the reviewer. The whole registry is the nightly run
(mutation-gate.yml, #513); a survivor files an issue (#472). The #693
post-mortem: 36 of 57 minutes of a one-line fix went to optional work.

- process-track.mjs: `mutants` is always false (no track, no label).
- classify-changes.mjs: Validate requests no diff mutants on any event;
  the `mutants` input stays so old `-f mutants=…` calls do not fail.
- _process.yml: the default for the gate and the merge is false.
- pre-push-gate.mjs: the manual run no longer runs mutants.
- Canon: PROCESS §2.7 (a mutant is written, not run; `--check` keeps the
  anchors), §5.1 (`ci:mutants` retired), §8 (ship/show: nothing beyond
  gate:small and the spec — one proof per item, no `--smokes` on ship,
  a stray flake is an issue, not an investigation), §10.4; AUTHOR,
  REVIEWER, AGENTS, TESTING.
- Registry: four mutants of the old request rules replaced by
  dev-mutants-requested-again and track-pays-for-mutants-again.

Issue: #709
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-09-29 23:04:41 +03:00
parent 18c9f8e77c
commit a8321e32cc
15 changed files with 143 additions and 152 deletions
+2 -2
View File
@@ -28,8 +28,8 @@ export const CI_PROOF_STATES = Object.freeze([
//
// #696: треки `show` и `ship` (PROCESS §5) мутантов по диффу до слияния не
// запрашивают — их доказательство лёгкое, `reviewLight`/`mergeLight`. Мутанты
// у них остаются в ночном полном реестре; `track:ask` и метка `ci:mutants`
// идут по прежним `review`/`merge`.
// у них остаются в ночном полном реестре. С #709 мутантов в разработке нет
// ни на одном треке: `review`/`merge` с мутантами конвейер больше не выбирает.
export const CI_PROOF_POLICIES = Object.freeze({
review: Object.freeze({ name: 'review', full: false, mutants: true }),
merge: Object.freeze({ name: 'merge', full: false, mutants: true }),
+4 -3
View File
@@ -121,9 +121,10 @@ export function isTaskBranch(refName) {
* CLI по-прежнему передаёт headMessage и fullInput вместе с остальным —
* функция их не читает, и тест закрепляет, что они НЕ влияют на ответ.
*/
export function mutantsRequested({ eventName, mutantsInput } = {}) {
if (eventName === 'pull_request') return true;
if (eventName === 'workflow_dispatch') return String(mutantsInput) === 'true';
export function mutantsRequested() {
// #709: мутанты проверяют тесты, а не продукт — в разработке Validate их не
// гоняет ни на dispatch (`mutants=true` больше ничего не включает), ни на PR.
// Весь реестр проверяет только ночной прогон (mutation-gate.yml).
return false;
}
+14 -39
View File
@@ -10389,8 +10389,8 @@ const MUTANT_DEFINITIONS = [
+ 'that softens it must fail, not pass as a shorter paraphrase (#634)',
patches: [{
file: 'docs/process/REVIEWER.md',
find: ' результатом прогона. Пустой третий столбец — находка Medium, а не\n примечание.',
replace: ' результатом прогона. Пустой третий столбец желательно заполнить.',
find: ' Пустой третий столбец — находка Medium, а не примечание. «Тест умеет',
replace: ' Пустой третий столбец желательно заполнить. «Тест умеет',
}],
},
{
@@ -12102,39 +12102,14 @@ const MUTANT_DEFINITIONS = [
}],
},
{
id: 'mutants-run-on-every-push',
guard: 'node --test --test-name-pattern="#510" test/classify-changes.test.mjs',
because: 'mutants by diff belong to the review candidate, the merge candidate and the PR — an '
+ 'ordinary push must not spend 3×8 runner minutes on them (#510 AC1, narrowed in #601)',
id: 'dev-mutants-requested-again',
guard: 'node --test --test-name-pattern="#709" test/classify-changes.test.mjs',
because: '#709 (owner, 2026-09-29): mutants check the tests, not the product; Validate runs no diff '
+ 'mutants on any event — the whole registry is the nightly run (mutation-gate.yml)',
patches: [{
file: 'scripts/classify-changes.mjs',
find: " if (eventName === 'workflow_dispatch') return String(mutantsInput) === 'true';\n return false;\n}",
replace: " if (eventName === 'workflow_dispatch') return String(mutantsInput) === 'true';\n return true; // mutant: every push\n}",
}],
},
{
id: 'mutants-run-on-beta-candidate',
guard: 'node --test --test-name-pattern="#601" test/classify-changes.test.mjs',
because: 'the `Release:` trailer requests the heavy gates, not the diff mutants (#601 AC1): by the '
+ 'beta candidate every issue has already been mutated on its review and merge candidates, and '
+ 'the trailer also lands on class-D baseline commits inside task branches (f342ccce) — six '
+ 'mutant jobs there prove nothing about tests that did not change',
patches: [{
file: 'scripts/classify-changes.mjs',
find: "export function mutantsRequested({ eventName, mutantsInput } = {}) {\n if (eventName === 'pull_request') return true;\n if (eventName === 'workflow_dispatch') return String(mutantsInput) === 'true';\n return false;\n}",
replace: "export function mutantsRequested({ eventName, headMessage, mutantsInput } = {}) {\n if (eventName === 'pull_request') return true;\n if (eventName === 'workflow_dispatch') return String(mutantsInput) === 'true';\n return hasReleaseTrailer(headMessage); // mutant: beta candidate\n}",
}],
},
{
id: 'mutants-run-on-full-dispatch',
guard: 'node --test --test-name-pattern="#601" test/classify-changes.test.mjs',
because: '`full=true` is the heavy set — smokes, golden, performance — and the nightly dispatch '
+ '(#601 AC1). Tying the diff mutants to it made a manual full run for a golden artifact pay six '
+ 'jobs the review conveyor then cancelled by concurrency, and duplicated the nightly registry',
patches: [{
file: 'scripts/classify-changes.mjs',
find: "export function mutantsRequested({ eventName, mutantsInput } = {}) {\n if (eventName === 'pull_request') return true;\n if (eventName === 'workflow_dispatch') return String(mutantsInput) === 'true';",
replace: "export function mutantsRequested({ eventName, fullInput, mutantsInput } = {}) {\n if (eventName === 'pull_request') return true;\n if (eventName === 'workflow_dispatch') return String(fullInput) === 'true' || String(mutantsInput) === 'true'; // mutant: full requests",
find: ' // Весь реестр проверяет только ночной прогон (mutation-gate.yml).\n return false;',
replace: ' // Весь реестр проверяет только ночной прогон (mutation-gate.yml).\n return true; // mutant',
}],
},
{
@@ -13599,14 +13574,14 @@ const MUTANT_DEFINITIONS = [
},
// #696: цена захода по треку — мутанты, рамки ship, пакетное ревью перед бетой.
{
id: 'track-show-pays-for-mutants',
guard: 'node --test --test-name-pattern="мутанты по диффу — только ask" test/process-track.test.mjs',
because: '#696: show/ship request no diff mutants before merge; only ask and the ci:mutants '
+ 'label do — otherwise every small task pays the 28-minute mutant run again',
id: 'track-pays-for-mutants-again',
guard: 'node --test --test-name-pattern="#709" test/process-track.test.mjs',
because: '#709: no track and no label buys a mutant run during development — the 28-minute '
+ 'run on ask and ci:mutants was the cost the owner removed',
patches: [{
file: 'scripts/process-track.mjs',
find: " const mutants = track === 'ask' || labels.includes('ci:mutants');",
replace: ' const mutants = true; // mutant: every track pays for mutants',
find: ' const mutants = false;',
replace: " const mutants = track === 'ask' || labels.includes('ci:mutants'); // mutant",
}],
},
{
+7 -21
View File
@@ -5,8 +5,8 @@
* node scripts/pre-push-gate.mjs --hook # из .githooks/pre-push, строки git на stdin
* node scripts/pre-push-gate.mjs
* node scripts/pre-push-gate.mjs --base origin/dev --head HEAD
* node scripts/pre-push-gate.mjs --no-smokes --no-mutants
* node scripts/pre-push-gate.mjs --max-smokes=3 --max-mutants=1
* node scripts/pre-push-gate.mjs --no-smokes
* node scripts/pre-push-gate.mjs --max-smokes=3
*
* Зачем. Красный CI — дорогой способ узнать о проблеме: пять минут ожидания, а
* при код-ревью ещё и лишний раунд. Прецедент назван в задаче: r2-H1 в #329
@@ -203,7 +203,6 @@ function manualGate(argv) {
const base = value('base', 'origin/dev');
const head = value('head', 'HEAD');
const maxSmokes = Number(value('max-smokes', 6));
const maxMutants = Number(value('max-mutants', 2));
const run = (label, command, args, options = {}) => {
const started = Date.now();
@@ -307,24 +306,11 @@ function manualGate(argv) {
}
}
// ---- мутанты по диффу -----------------------------------------------------
if (flag('no-mutants')) {
skipped.push('мутанты — запрошено --no-mutants');
} else {
const list = capture('node', ['scripts/mutation-gate.mjs', '--changed=' + `${base}..${head}`, '--check']);
const touched = Number(/мутантов затронуто (\d+)/.exec(list.stdout || '')?.[1] ?? -1);
if (touched === 0) {
console.log('\n── Мутанты\n дифф не задевает ни одного patch.file');
} else if (touched < 0) {
skipped.push('мутанты — не удалось определить выборку по диффу');
} else if (touched > maxMutants) {
skipped.push(`мутанты (${touched}) — больше лимита ${maxMutants};`
+ ` каждый пересобирает бандл. Прогон: node scripts/mutation-gate.mjs --changed=${base}..${head}`);
} else {
steps.push(run('Мутанты по диффу', 'node',
['scripts/mutation-gate.mjs', `--changed=${base}..${head}`]));
}
}
// ---- мутанты ---------------------------------------------------------------
// #709: мутанты проверяют тесты, а не продукт, и в разработке не гоняются —
// весь реестр проверяет ночной прогон (mutation-gate.yml). Флаги
// --no-mutants и --max-mutants приняты для совместимости и ничего не меняют.
skipped.push('мутанты — в разработке не гоняются (#709), весь реестр проверяет ночь');
// ---- вердикт --------------------------------------------------------------
const failed = steps.filter((step) => !step.ok);
+6 -3
View File
@@ -7,7 +7,8 @@
*
* `resolve` печатает `track=ship|show|ask`, `mutants=true|false` и
* `full=true|false` — то, что конвейер ревью читает, решая, сколько стоит
* заход: мутанты по диффу нужны только `ask` и метке `ci:mutants`; полный
* заход: мутантов в разработке нет ни на одном треке (#709) — `mutants`
* всегда `false`, весь реестр проверяет только ночной прогон; полный
* набор (смоки, golden, perf) на ветке задачи — только меткам `ci:full` и
* `ci:golden` (#697). Инфраструктурная задача без трековой
* метки — `show` (§5.1); признак инфраструктуры механический, как в §1: в
@@ -47,13 +48,15 @@ export const hasTrackLabel = (labels = []) => ['track:ship', 'track:show', 'trac
/**
* Трек, по которому конвейер оценивает заход. Явная метка решает всё; без неё
* инфраструктурная задача (ни одного файла класса A в диффе) — `show`, прочие —
* `ask`. Мутанты по диффу — только на `ask` или по метке `ci:mutants`.
* `ask`. Мутанты проверяют тесты, а не продукт: в разработке их не гоняют ни
* локально, ни в CI (#709, решение владельца 2026-09-29) — только ночной полный
* реестр (`mutation-gate.yml`, #513). Поле остаётся для совместимости выхода.
* Полный набор — по меткам `ci:full` и `ci:golden` на любом треке (#697).
*/
export function resolveTrack({ labels = [], files = [] } = {}) {
const infrastructure = files.length > 0 && files.every((file) => classify(file) !== 'A');
const track = hasTrackLabel(labels) ? trackFromLabels(labels) : (infrastructure ? 'show' : 'ask');
const mutants = track === 'ask' || labels.includes('ci:mutants');
const mutants = false;
const full = labels.includes('ci:full') || labels.includes('ci:golden');
return { track, mutants, full, infrastructure };
}