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
+6 -7
View File
@@ -413,8 +413,8 @@ jobs:
echo "МАТЕРИАЛ НЕ ЗАПУШЕН" >> "$GITHUB_STEP_SUMMARY"
fi
# Трек решает цену захода (PROCESS.md §5, §10.4, #696): мутанты по диффу —
# только `track:ask` и метка `ci:mutants`; `ship` сливается без модели,
# Трек решает цену захода (PROCESS.md §5, §10.4, #696): мутантов в
# разработке нет ни на одном треке (#709); `ship` сливается без модели,
# если правка укладывается в механические рамки. Рамки судят дифф от
# merge-base с dev — работу задачи, а не то, что принёс dev, — поэтому шаг
# стоит до ребейза. Выход за рамки повышает трек до `show` здесь же:
@@ -702,9 +702,8 @@ jobs:
--add-label S6-in-progress --remove-label S7-code-review
echo "S7-code-review -> S6-in-progress (ревью не запускалось)"
# Мутанты по диффу бегут только по запросу (#510): до ревью конвейер
# запускает Validate на материале и ждёт его — с мутантами на `ask` и по
# метке `ci:mutants`, лёгкий на `show`/`ship` (#696). Лёгкому годится и
# До ревью конвейер запускает Validate на материале и ждёт его — всегда
# лёгкий: мутантов в разработке нет (#709), весь реестр — ночью. Лёгкому годится и
# завершённый push-прогон на том же SHA: dispatch повторил бы его. Красный или
# пропавший прогон возвращает задачу автору без ревью — цикл не
# тратится на код, который CI уже отверг (08.09: #437 дважды ушёл в S6
@@ -737,7 +736,7 @@ jobs:
# прогон гейт и без ожидания возвращает сразу.
set +e
node scripts/validate-gate.mjs --repo="${{ github.repository }}" --ref="$BRANCH" --sha="$SHA" --no-wait \
--mutants="${MUTANTS:-true}" --full="${FULL:-false}"
--mutants="${MUTANTS:-false}" --full="${FULL:-false}"
code=$?
set -e
case "$code" in
@@ -1753,7 +1752,7 @@ jobs:
exit 0
fi
node scripts/merge-candidate.mjs --branch="$BRANCH" --material="$MATERIAL_SHA" \
--issue="$NUM" --repo="${{ github.repository }}" --mutants="${MUTANTS:-true}"
--issue="$NUM" --repo="${{ github.repository }}" --mutants="${MUTANTS:-false}"
- name: Переставить метку
if: steps.ready.outputs.proceed == 'true'
+8 -5
View File
@@ -21,12 +21,11 @@ on:
description: 'Полный набор: смоки, golden, performance_smoke (без мутантов — #601)'
type: boolean
default: true
# Мутанты по диффу бегут только по явному запросу (#510, #601):
# ревью-конвейер и слияние кандидата запускают `-f full=false
# -f mutants=true` на материале. `full=true`, трейлер `Release:` и ночь
# их не включают — ночью идёт полный реестр (mutation-gate.yml).
# #709: мутанты в разработке не гоняются — вход оставлен, чтобы старые
# вызовы `-f mutants=…` не падали, но job мутантов по диффу он больше не
# включает (classify-changes.mjs). Весь реестр — ночью (mutation-gate.yml).
mutants:
description: 'Мутанты по диффу на этом SHA (кандидат ревью или слияния)'
description: 'Не действует с #709: мутанты — только ночной полный прогон'
type: boolean
default: false
@@ -650,6 +649,10 @@ jobs:
# слияние кандидата запускают Validate по кнопке с `mutants=true` на
# материале; PR берёт её сам. За 08–09.09 на промежуточных пушах она стоила
# 48 из 56 часов job-минут и в основном отменялась следующим пушем. С #601
# С #709 job не запрашивает никто: мутанты в разработке не гоняются, весь
# реестр проверяет ночь (mutation-gate.yml). Job оставлен до отдельной уборки
# вместе с контрактом имён job (#622); `mutants_requested` всегда false.
# Ниже — история до #709.
# её не запрашивают ни `full=true`, ни кандидат беты (`Release:`): к бете
# каждая задача уже прогнана мутантами на ревью и на слитом кандидате, а
# ночь покрыта полным реестром. Когда мутанты запрошены, job бежит даже при
+3 -2
View File
@@ -84,8 +84,9 @@ a spec review. Any agent may raise a track with a reason; only the owner lowers
class A file — skips analysis and spec and enters at `S7-code-review`
(`PROCESS.md` §1). Every change is code-reviewed; on `ship` the review moves to a
batch review of the beta range before the tag (`ship-review.yml`, `PROCESS.md`
§11.7). The review pipeline prices each round by track (§10.4): diff mutants
only on `ask` or with `ci:mutants`; a rebase before review only on `ask` or when
§11.7). The review pipeline prices each round by track (§10.4): no mutants run
during development on any track — the whole registry runs nightly (#709); a
rebase before review only on `ask` or when
the branch does not merge cleanly into `dev`. Review checks scope, risks and the
evidence from executed tests, but does not replace executing them.
+47 -37
View File
@@ -245,20 +245,25 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready →
- **Защитный AC доказывается таблицей «чем краснеет» (#435).** Для каждого AC,
заявляющего защиту — валидация, гард, лимит, отказ, инвариант, — в документе
ревью обязательна строка из трёх столбцов: **AC · чем доказан** (точная
команда или имя теста) **· чем краснеет** — мутация, снятая защита или
отрицательная проба, с результатом прогона. Пустой третий столбец — находка
Medium, а не примечание.
команда или имя теста) **· чем краснеет** — мутант в реестре или
отрицательный случай в самом тесте; прогон мутанта даёт ночь (#709). Пустой
третий столбец — находка Medium, а не примечание.
«Тест умеет падать» без названной мутации и её вывода доказательством не
является. Аудит v1.71.0-beta.1 нашёл пять контрактов #51 и #423, где тест
«Тест умеет падать» без названной мутации доказательством не является; её
поимку подтверждает ночной прогон (#709). Аудит v1.71.0-beta.1 нашёл пять контрактов #51 и #423, где тест
оставался зелёным на снятой защите; все пять прошли код-ревью как доказанные,
а два теста были записаны в закрытие coverage-ratchet под именами, обещавшими
то, чего они не проверяли (#430).
Мутант в реестре `scripts/mutation-registry.mjs` обязателен, когда защита живёт в
продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden): там
ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов
достаточно прогона со снятой защитой, приведённого в документе.
продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden). Для
чистых юнитов достаточно отрицательного случая в самом тесте.
**Мутант пишется, но в разработке не гоняется** — ни локально, ни в CI, ни
ревьюером (#709, решение владельца 2026-09-29). Мутанты проверяют тесты, а не
продукт, и их цена — десятки минут на задачу. Якоря реестра сверяет статический
`mutation-gate --check`. Поимку проверяет только ночной полный прогон
(`mutation-gate.yml`, #513); выживший мутант ночью заводит issue (#472).
Новый мутант с browser-smoke guard допустим только когда инвариант нельзя
доказать без браузера: `because` обязан назвать конкретную зависимость от
@@ -620,10 +625,10 @@ patch-id кандидата слияния: вердикт к работе за
|---|---|
| `ci:full` | полный Validate на ветке задачи |
| `ci:golden` | golden на ветке и приёмку сдвинутых кадров в самой задаче |
| `ci:mutants` | мутанты по диффу на кандидате ревью и слияния |
Как конвейер читает метки, описывает §10.4: `ci:mutants` (#696), `ci:full` и
`ci:golden` — dispatch Validate с `full=true` на материале ревью (#697).
Как конвейер читает метки, описывает §10.4: `ci:full` и `ci:golden` — dispatch
Validate с `full=true` на материале ревью (#697). Метка `ci:mutants` снята
(#709): мутанты в разработке не гоняются ни на каком треке.
**Прежние метки.** `trivial` и `small` читаются как `track:show`; продуктовая
задача без трековой метки — как `track:ask`; инфраструктурная задача (§1) без
@@ -759,6 +764,17 @@ typecheck, юниты, целостность и бюджет бандла, `no-
`lint:unused` и вывод `smoke-select`. Команды ниже — то же самое по отдельности
плюс то, что по диффу и AC:
**Цена `ship` и `show` — без добровольных надбавок** (#709, разбор #693: из 57
минут однострочного фикса 36 ушли на необязательное). Сверх `gate:small` и
названного в ТЗ автор ничего не прогоняет:
- мутанты не гоняются (§2.7);
- на пункт ТЗ — одно доказательство, дублей вида «смок и юнит на одно и то же» нет;
- `gate:small -- --smokes` на `ship` не нужен, на `show` — только смоки из вывода
`smoke-select` и названные в AC;
- флак, найденный по ходу, — отдельное issue одной записью: что упало, где,
как часто. Расследование в рамках задачи не ведётся.
```
npm run gate:small # обязательная часть одной командой
npx tsc --noEmit
@@ -930,7 +946,7 @@ Performance зелёные на точном SHA, плюс зелёный E2E н
| `rejected` | Отклонено, issue закрыт |
Модификаторы: трек `track:ship`/`track:show`/`track:ask` (§5), тяжёлые проверки
`ci:full`/`ci:golden`/`ci:mutants` (§5.1), `hotfix`, `process`, `review-4`;
`ci:full`/`ci:golden` (§5.1), `hotfix`, `process`, `review-4`;
приоритет `P1`/`P2`/`P3`; тип `bug`/`feature`/`tech-debt`. Прежние `small` и
`trivial` читаются как `track:show` (§5.1).
Тематические метки (`polish`, `infra`, `tests`, `docs`, `security`, `vacuum`)
@@ -1166,12 +1182,12 @@ Matysh/houseplan-card/.github/workflows/_<имя>.yml@dev` с `secrets: inherit`
| | `ship` | `show` | `ask` |
|---|---|---|---|
| Validate на материале | лёгкий; годится завершённый push-прогон на этом SHA | лёгкий, как у `ship` | с мутантами по диффу, dispatch |
| Validate на материале | лёгкий; годится завершённый push-прогон на этом SHA | лёгкий, как у `ship` | лёгкий, как у `ship` (#709) |
| Ребейз до ревью | нет, если `git merge-tree` с `dev` чистый | нет, если чистый | да (#257) |
| Ревью модели | нет — пакетное ревью перед бетой (§11.7) | корректность и AC; окружение — по нужде | полное |
| Проверка кандидата слияния | ждёт push-прогон кандидата; dispatch — только если его нет | как у `ship` | dispatch с мутантами |
| Проверка кандидата слияния | ждёт push-прогон кандидата; dispatch — только если его нет | как у `ship` | как у `ship` (#709) |
Метка `ci:mutants` возвращает мутанты по диффу на любом треке. Рамки `ship`
Мутантов по диффу нет ни на одном треке (#709, §2.7). Рамки `ship`
(§5) проверяет тот же шаг; выход за них — комментарий в issue и замена
`track:ship` на `track:show` в этом же заходе. Слияние `ship` оставляет в issue
комментарий с машинным маркером `hp:ship-merge`. Это не вердикт ревью и так себя
@@ -1186,14 +1202,14 @@ Matysh/houseplan-card/.github/workflows/_<имя>.yml@dev` с `secrets: inherit`
**Ревью `show` судит корректность и AC.** Medium — дефект поведения, который
увидит пользователь, или невыполненный AC. Бухгалтерия — нет мутанта или записи
в реестре, нечувствительный тест на побочный вызов, формулировка в документе —
Low и цикла не открывает. Отсутствие мутантов по диффу на `show` не находка:
полный реестр гоняется ночью. Ревью `show` не ставит Chromium, если тело issue
Low и цикла не открывает. Отсутствие прогона мутантов не находка ни на каком
треке: полный реестр гоняется ночью (#709). Ревью `show` не ставит Chromium, если тело issue
не называет смоук или браузер. Ревью ТЗ не ставит ни `npm ci`, ни браузер: кода
оно не исполняет.
**Ревью не начинается на красном коде** (#510). После фиксации материала конвейер
запускает Validate с мутантами по диффу на этом SHA (`scripts/validate-gate.mjs`:
`workflow_dispatch validate.yml -f mutants=true`). **Ждёт его не раннер, а событие**
берёт завершённый push-прогон Validate на этом SHA или запускает лёгкий
(`scripts/validate-gate.mjs`: `workflow_dispatch validate.yml`). **Ждёт его не раннер, а событие**
(#636): подготовка убеждается, что dispatch встал на материал, кладёт запечатанный
маркер ожидания `review-pending-…` и завершается; по завершении Validate
`process-resume.yml` (`workflow_run`) переставляет метку `S7-code-review`, и новый
@@ -1202,18 +1218,13 @@ Low и цикла не открывает. Отсутствие мутантов
завершённым Validate он будит повторной меткой, без маркера — как прежде, только
диагностика. Будить без маркера нельзя: это второй вызов модели. Красный или
пропавший прогон возвращает задачу в `S6-in-progress` с комментарием и ссылкой —
код никто не читал, цикл ревью не израсходован. Мутанты по диффу вообще бегут
только по явному запросу: на кандидате ревью, кандидате слияния (#492) — оба
диспатчат Validate с `mutants=true` — и на PR, где Validate единственный сигнал;
обычный push обходится дешёвыми гейтами (~3 минуты). За 08–09.09 мутанты на
каждом промежуточном пуше стоили 48 из 56 часов job-минут Validate и в основном
отменялись следующим пушем. **Кандидат беты (`Release:`), `full=true` и ночь
мутантов не запрашивают** (#601, решение владельца 20.09): мутационный гейт
проверяет тесты, а не продукт (#513), к бете каждая задача прогнана им дважды —
на ревью и на слитом после ребейза кандидате, — а ночью идёт полный реестр
(`mutation-gate.yml`, 00:43 UTC). Релизный гейт (#541) требует полного Validate,
но не mutant-jobs; для ревью и слияния шесть исполненных mutant-jobs остаются
обязательными.
код никто не читал, цикл ревью не израсходован. **Мутанты по диффу в
разработке не бегут вовсе** (#709): ни на кандидате ревью, ни на кандидате
слияния, ни на PR, ни по кнопке. До #709 их запрашивали кандидаты ревью и
слияния на `ask` (#510, #601), и заход стоил около 28 минут; к бете каждая
задача прогонялась ими дважды. Мутанты проверяют тесты, а не продукт (#513), —
весь реестр проверяет ночь (`mutation-gate.yml`, 00:43 UTC). Ни ревью, ни
слияние, ни релизный гейт (#541) mutant-jobs не требуют.
Ожидание gates, работа модели и публикация/интеграция — три независимых jobs
(#551) с отдельными бюджетами 55, 45 и 55 минут. Поэтому долгий Validate не
@@ -1298,12 +1309,11 @@ npm ci, Python и Chromium, оставаясь исполненной job: до
- если двигался — ребейз (конфликт — `S6-in-progress`, как раньше), сравнение
patch-id проверенного и получившегося диффа (различие — `S7-code-review`: вердикт
к другому диффу не применим, §2.10; метка снимается и ставится заново, чтобы
событие запустило новый заход, #706), публикация кандидата в ветку задачи, запуск
Validate с мутантами на ней (#510) и ожидание зелёного dispatch-прогона **на этом
SHA** — push-прогон мутантов не несёт — и только затем push в `dev` с lease на ту
вершину, поверх которой кандидат собран. На `show`/`ship` мутантов нет, и
лёгкий Validate кандидата уже запустил сам push в ветку: слияние ждёт этот
push-прогон, а dispatch шлёт, только если его нет за три минуты (#696). Отклонённый lease — `dev` двинулся снова
событие запустило новый заход, #706), публикация кандидата в ветку задачи и
ожидание зелёного лёгкого Validate **на этом SHA** — его запускает сам push в
ветку, dispatch — только если прогона нет за три минуты (#696, #709), — и только
затем push в `dev` с lease на ту вершину, поверх которой кандидат собран.
Отклонённый lease — `dev` двинулся снова
— новая попытка; после третьей — `S6-in-progress` с комментарием;
- красный Validate на кандидате или прогон, не появившийся за три минуты, —
`S6-in-progress` с ссылкой; `S8-merged` ставится только после push;
+12 -6
View File
@@ -111,8 +111,15 @@ browser/backend guard нельзя. Все недоказанные исходы
рантайме, но не статически: `if (eps < 0)`, `String(mode) === 'mutant-never-…'`,
`if (walls.length >= 0) return walls.slice();`. В `.mjs` этого ограничения нет.
В CI `changed_mutants` бежит не на каждом пуше, а по явному запросу (#510,
сужено в #601): `workflow_dispatch validate.yml -f mutants=true` (его делают
**С #709 мутанты в разработке не гоняются вовсе** — ни локально, ни в CI, ни
ревьюером; весь реестр проверяет ночной прогон (`mutation-gate.yml`), выживший
мутант заводит issue. Мутант своей защиты пишется в реестр, якоря сверяет
`mutation-gate --check`; `--id` и `--changed` остаются инструментами разбора
ночного сбоя, а не гейтом задачи. Job `changed_mutants` в Validate не
запрашивает никто (`mutantsRequested` всегда `false`). Ниже — история до #709.
До #709 в CI `changed_mutants` бежал не на каждом пуше, а по явному запросу (#510,
сужено в #601): `workflow_dispatch validate.yml -f mutants=true` (его делали
ревью-конвейер на материале ревью и слияние на кандидате) и PR. `full=true`
(ночь, кнопка), кандидат беты (трейлер `Release:`) и обычный push мутантов не
запрашивают: за 08–09.09 мутанты на промежуточных пушах стоили 48 из 56 часов
@@ -257,7 +264,7 @@ c._drag = { id, sx, sy }; // private-ok: #NNN состояние жеста, о
`scripts/gate-small.mjs` и нигде больше не переписывается (#701). Хук `pre-push`
гоняет его сам для веток задач — таблица «В хуке» ниже. Ручной
`node scripts/pre-push-gate.mjs` из этого раздела — расширенный прогон:
типы, юниты, смоки и мутанты по диффу.
типы, юниты и смоки; мутантов в нём с #709 нет.
Красный CI — дорогой способ узнать о проблеме: пять минут ожидания, а при
код-ревью ещё и лишний раунд. Прецедент назван в задаче: находка r2-H1 в #329
@@ -298,8 +305,7 @@ node scripts/pre-push-gate.mjs --max-smokes=3 --max-mutants=1
свежесть проверяет сам продукт — `assertFreshDemoBundle` внутри каждого смока
сверяет вшитый отпечаток с исходниками дерева и скажет, если нужна пересборка.
Лимиты по умолчанию — шесть смоков и два мутанта. Мутант дорог: каждый
пересобирает бандл, а правка `src/houseplan-card.ts` задевает их 62. Превышение
Лимит по умолчанию — шесть смоков; мутантов ручной режим с #709 не гоняет. Превышение
лимита не проглатывается — набор печатает точную команду для полного прогона.
Три вида ответа `smoke-select` различаются и здесь: дифф без исполняемого кода —
@@ -332,7 +338,7 @@ node scripts/pre-push-gate.mjs --max-smokes=3 --max-mutants=1
командой, а пушат с `HP_PREPUSH_GATE=0` — и пишут об этом в хендоффе.
Ручной режим выше (`node scripts/pre-push-gate.mjs` без `--hook`) остался как был:
tsc, юниты, смоки и мутанты по диффу. Решение хука покрыто
tsc, юниты и смоки (мутантов с #709 нет). Решение хука покрыто
`test/pre-push-gate.test.mjs`, включая настоящий `.githooks/pre-push` на
временном репозитории.
+11 -4
View File
@@ -59,8 +59,8 @@
- `track:ship`: `S1-new` → `S5-ready`, в теле issue строка «что меняется и чем
проверить». Рамки: дифф `src/**` до 30 строк, без новых файлов, i18n, полей
конфига и Python ([§5](../../PROCESS.md#5-треки-ship-show-ask--метка-владельца)).
- Тяжёлые проверки на любом треке — метками `ci:full`, `ci:golden`,
`ci:mutants`; прежние `small` и `trivial` читаются как
- Тяжёлые проверки на любом треке — метками `ci:full` и `ci:golden`;
прежние `small` и `trivial` читаются как
`track:show` ([§5.1](../../PROCESS.md#51-метки-тяжёлых-проверок-и-прежние-метки)).
## ТЗ (`S3-spec`)
@@ -150,6 +150,13 @@
`pytest tests_backend`, junction parity; `golden:verify` — только с меткой
`ci:golden` ([§8](../../PROCESS.md#8-гейты); `docs/TESTING.md`, «Локальный
набор перед пушем»).
- Мутанты не гоняются ни локально, ни в CI: мутант своей защиты пишется в
реестр, якорь сверяет `mutation-gate --check`, поимку — ночь (#709,
[§2.7](../../PROCESS.md#27-код-ревью)).
- На `ship` и `show` сверх `gate:small` и названного в ТЗ ничего не гоняется:
одно доказательство на пункт ТЗ; `--smokes` на `ship` не нужен; попутный флак —
отдельное issue одной записью, без расследования в задаче
([§8](../../PROCESS.md#8-гейты)).
- Бандл в коммит задачи не идёт: сборка переписывает отслеживаемый `dist/`,
перед коммитом — `npm run bundle:clean`; хук `commit-msg` отклоняет пути
бандла без трейлера `Release:` (#657, [§1](../../PROCESS.md#1-основное-правило)).
@@ -179,8 +186,8 @@
`node scripts/process-gate.mjs --issues`; после `S7-code-review` в ветку не
пушить до вердикта; `S7` ставится один раз на заход
([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер)).
- Ревью не начинается на красном коде: конвейер сам гоняет Validate — с
мутантами на `ask`, лёгкий на `show`/`ship` — и возвращает красный в
- Ревью не начинается на красном коде: конвейер сам гоняет лёгкий Validate
и возвращает красный в
`S6-in-progress` без траты цикла
([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер)).
- Ветка приводится к `dev` до ревью, а не после: конфликт — возврат в
+8 -6
View File
@@ -50,10 +50,10 @@
([§2.7](../../PROCESS.md#27-код-ревью),
[§2.6](../../PROCESS.md#26-в-разработке--реализация)).
- Защитный AC доказывается таблицей «чем краснеет»: AC · чем доказан ·
чем краснеет — мутация, снятая защита или отрицательная проба с
результатом прогона. Пустой третий столбец — находка Medium, а не
примечание. «Тест умеет падать» без названной мутации и её вывода
доказательством не является ([§2.7](../../PROCESS.md#27-код-ревью)).
чем краснеет — мутант в реестре или отрицательный случай в самом тесте.
Пустой третий столбец — находка Medium, а не примечание. «Тест умеет
падать» без названной мутации доказательством не является
([§2.7](../../PROCESS.md#27-код-ревью)).
- Вердикт привязан к SHA (#312): числа и факты сверяются с
`git rev-parse HEAD` перед итогом; более новый коммит, которого нет в
материале, — находка, а не повод его подтянуть
@@ -138,8 +138,10 @@
записи в реестре, нечувствительный тест на побочный вызов — Low и цикла не
открывает ([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер),
[§5](../../PROCESS.md#5-треки-ship-show-ask--метка-владельца)).
- Мутанты по диффу на `show` не запрашиваются; их отсутствие не находка
([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер)).
- Мутанты в разработке не гоняются ни на каком треке — ревьюер их тоже не
применяет; проверяет, что защита названа мутантом в реестре. Отсутствие прогона
не находка: поимку проверяет ночь (#709,
[§2.7](../../PROCESS.md#27-код-ревью)).
- Ветка `show` с чистым слиянием к `dev` до ревью не приводится: материал —
ветка как есть, кандидат проверит Validate при слиянии
([§10.4](../../PROCESS.md#104-событийный-конвейер-метка-как-триггер)).
+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 };
}
+7 -8
View File
@@ -184,10 +184,9 @@ test('CLI --heavy читает событие и сообщение из окр
// #601: кандидат беты и полный набор берут тяжёлые job, но не мутантов
assert.equal(run({ EVENT_NAME: 'push', HEAD_MESSAGE: 'x\n\nRelease: v1.2.3' }), 'heavy=true\nmutants_requested=false');
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=true\nmutants_requested=false');
// #510: мутанты по кнопке без полного набора — вызов конвейера ревью и слияния
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'false', MUTANTS_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=false\nmutants_requested=true');
// обе кнопки разом — полный набор И мутанты; так делает только человек, конвейер шлёт full=false
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'true', MUTANTS_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=true\nmutants_requested=true');
// #709: `mutants=true` по кнопке мутантов больше не включает — только ночь
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'false', MUTANTS_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=false\nmutants_requested=false');
assert.equal(run({ EVENT_NAME: 'workflow_dispatch', FULL_INPUT: 'true', MUTANTS_INPUT: 'true', HEAD_MESSAGE: '' }), 'heavy=true\nmutants_requested=false');
});
test('#586: режим гейта скриншотов приходит одним значением и на кандидате строгий', () => {
@@ -218,11 +217,11 @@ test('#586: preflight спрашивает режим одним значени
assert.match(workflow, /check-docs\.mjs "\$external" --screenshots=\$mode/);
});
test('#510 AC1 / #601 AC1: мутанты по диффу запрашиваются только кнопкой mutants=true и PR — не пушем, не кандидатом беты, не full', () => {
test('#709: Validate не запрашивает мутантов по диффу ни на одном событии — весь реестр ночью', () => {
const t = (env) => mutantsRequested(env);
assert.equal(t({ eventName: 'push', headMessage: 'fix: x\n\nIssue: #1\nUser-Visible: no' }), false, 'обычный push');
assert.equal(t({ eventName: 'pull_request' }), true, 'на PR Validate — единственный сигнал');
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'false', mutantsInput: 'true' }), true, 'конвейер ревью и слияния');
assert.equal(t({ eventName: 'pull_request' }), false, 'PR — тоже нет');
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'false', mutantsInput: 'true' }), false, 'кнопка mutants=true больше ничего не включает');
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'false', mutantsInput: 'false' }), false, 'кнопка без запроса');
// #601: мутанты проверяют тесты, а не продукт (#513); к бете задача прогнана
// ими дважды, а ночь покрыта полным реестром — эти входы их не включают.
@@ -231,7 +230,7 @@ test('#510 AC1 / #601 AC1: мутанты по диффу запрашивают
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'true' }), false, 'полный набор по кнопке — без мутантов');
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'true', mutantsInput: 'false' }), false);
assert.equal(t({ eventName: 'schedule' }), false, 'расписание Validate — не место мутантов, реестр идёт в mutation-gate.yml');
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'true', mutantsInput: 'true' }), true, 'явный запрос действует и рядом с full');
assert.equal(t({ eventName: 'workflow_dispatch', fullInput: 'true', mutantsInput: 'true' }), false, 'и рядом с full');
// #601 AC2: тяжёлые гейты на тех же входах не изменились
assert.equal(heavyGatesRequested({ eventName: 'push', headMessage: 'x\n\nRelease: v1.2.3' }), true);
assert.equal(heavyGatesRequested({ eventName: 'workflow_dispatch', fullInput: 'true' }), true);
+1 -1
View File
@@ -80,7 +80,7 @@ const KEY_RULES = {
['27-код-ревью', 'проверено чтением, не исполнением'],
['27-код-ревью', 'Защитный AC доказывается таблицей «чем краснеет»'],
['27-код-ревью', 'Пустой третий столбец — находка Medium, а не примечание'],
['27-код-ревью', '«Тест умеет падать» без названной мутации и её вывода доказательством не является'],
['27-код-ревью', '«Тест умеет падать» без названной мутации доказательством не является'],
['27-код-ревью', 'Вердикт привязан к SHA (#312)'],
['27-код-ревью', 'новое имя в нём — находка ревью, а не запись в список'],
['8-гейты', 'ревьюер обязан перечислить, какие гейты прогнал, какие нет и почему'],
+7 -8
View File
@@ -21,7 +21,7 @@ test('пакет задачи и конвейер читают трек одно
test('явная трековая метка главнее признака инфраструктуры (#696)', () => {
const infra = ['scripts/x.mjs', '.github/workflows/y.yml'];
assert.deepEqual(resolveTrack({ labels: ['track:ask'], files: infra }), { track: 'ask', mutants: true, full: false, infrastructure: true });
assert.deepEqual(resolveTrack({ labels: ['track:ask'], files: infra }), { track: 'ask', mutants: false, full: false, infrastructure: true });
assert.deepEqual(resolveTrack({ labels: ['track:ship'], files: ['src/a.ts'] }), { track: 'ship', mutants: false, full: false, infrastructure: false });
assert.equal(resolveTrack({ labels: ['small'], files: ['src/a.ts'] }).track, 'show');
});
@@ -34,11 +34,10 @@ test('без трековой метки инфраструктура — show,
assert.equal(hasTrackLabel(['trivial']), true);
});
test('мутанты по диффу — только ask и метка ci:mutants (#696)', () => {
assert.equal(resolveTrack({ labels: ['track:show'], files: ['src/a.ts'] }).mutants, false);
assert.equal(resolveTrack({ labels: ['track:show', 'ci:mutants'], files: ['src/a.ts'] }).mutants, true);
assert.equal(resolveTrack({ labels: ['track:ship', 'ci:mutants'], files: ['src/a.ts'] }).mutants, true);
assert.equal(resolveTrack({ labels: [], files: ['src/a.ts'] }).mutants, true);
test('#709: мутантов в разработке нет ни на одном треке и ни по какой метке', () => {
for (const labels of [['track:ship'], ['track:show'], ['track:ask'], ['track:show', 'ci:mutants'], ['ci:mutants'], []]) {
assert.equal(resolveTrack({ labels, files: ['src/a.ts'] }).mutants, false, labels.join(',') || 'без меток');
}
});
test('рамки ship: строки src/** считаются вместе, граница включительна (#696)', () => {
@@ -141,7 +140,7 @@ test('конвейер: трек снимается до ребейза, мут
assert.match(rebaseStep, /if \[ "\$TRACK" = "show" \] \|\| \[ "\$TRACK" = "ship" \]; then\n\s+if git merge-tree --write-tree origin\/dev HEAD/,
'show/ship не ребейзятся при чистом слиянии');
const gateStep = workflow.slice(gate, at(' - name: Validate идёт — раунд продолжит событие\n'));
assert.match(gateStep, /--mutants="\$\{MUTANTS:-true\}"/, 'по умолчанию — с мутантами');
assert.match(gateStep, /--mutants="\$\{MUTANTS:-false\}"/, 'по умолчанию — без мутантов (#709)');
assert.match(gateStep, /MUTANTS: \$\{\{ steps\.track\.outputs\.mutants \}\}/);
});
@@ -162,7 +161,7 @@ test('конвейер: ship в рамках сливается без моде
assert.doesNotMatch(decide.slice(decide.indexOf('SHIP" = "true'), decide.indexOf('elif [ "$REUSE"')), /Вердикт:/,
'слияние без ревью не называет себя вердиктом ревью');
const merge = integrate.slice(integrate.indexOf('- name: Слить ветку в dev'), integrate.indexOf('- name: Переставить метку'));
assert.match(merge, /--mutants="\$\{MUTANTS:-true\}"/);
assert.match(merge, /--mutants="\$\{MUTANTS:-false\}"/);
const env = modelJob.slice(modelJob.indexOf('- name: Что ревьюеру нужно из окружения'), modelJob.indexOf('- name: Установить Claude Code'));
assert.match(env, /if \[ "\$STAGE" = "spec" \]; then deps=false; browser=false; fi/, 'ревью ТЗ не ставит окружение');
assert.match(env, /if: steps\.env_needs\.outputs\.deps == 'true'\n\s+run: npm ci/);