diff --git a/.github/workflows/_process.yml b/.github/workflows/_process.yml index 40742255..5f66f098 100644 --- a/.github/workflows/_process.yml +++ b/.github/workflows/_process.yml @@ -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' diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a307016d..10d25c1b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 бежит даже при diff --git a/AGENTS.md b/AGENTS.md index 210cd886..4be85a1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/PROCESS.md b/PROCESS.md index db83806b..d17e11e1 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -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; diff --git a/docs/TESTING.md b/docs/TESTING.md index f130c42b..326a8769 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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` на временном репозитории. diff --git a/docs/process/AUTHOR.md b/docs/process/AUTHOR.md index b5f65f7f..11244ec8 100644 --- a/docs/process/AUTHOR.md +++ b/docs/process/AUTHOR.md @@ -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` до ревью, а не после: конфликт — возврат в diff --git a/docs/process/REVIEWER.md b/docs/process/REVIEWER.md index cc12d09e..63a2a69b 100644 --- a/docs/process/REVIEWER.md +++ b/docs/process/REVIEWER.md @@ -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-событийный-конвейер-метка-как-триггер)). diff --git a/scripts/ci-proof.mjs b/scripts/ci-proof.mjs index 0138ec91..ebb0a5ee 100644 --- a/scripts/ci-proof.mjs +++ b/scripts/ci-proof.mjs @@ -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 }), diff --git a/scripts/classify-changes.mjs b/scripts/classify-changes.mjs index 0aba6641..0c925102 100755 --- a/scripts/classify-changes.mjs +++ b/scripts/classify-changes.mjs @@ -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; } diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index 5c1cb4ec..e24e0128 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -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", }], }, { diff --git a/scripts/pre-push-gate.mjs b/scripts/pre-push-gate.mjs index 62b8b091..58354966 100644 --- a/scripts/pre-push-gate.mjs +++ b/scripts/pre-push-gate.mjs @@ -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); diff --git a/scripts/process-track.mjs b/scripts/process-track.mjs index 10901093..4190e0ad 100644 --- a/scripts/process-track.mjs +++ b/scripts/process-track.mjs @@ -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 }; } diff --git a/test/classify-changes.test.mjs b/test/classify-changes.test.mjs index c92fdbda..5047cdd6 100644 --- a/test/classify-changes.test.mjs +++ b/test/classify-changes.test.mjs @@ -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); diff --git a/test/process-digests.test.mjs b/test/process-digests.test.mjs index 5600eccd..0c3b144c 100644 --- a/test/process-digests.test.mjs +++ b/test/process-digests.test.mjs @@ -80,7 +80,7 @@ const KEY_RULES = { ['27-код-ревью', 'проверено чтением, не исполнением'], ['27-код-ревью', 'Защитный AC доказывается таблицей «чем краснеет»'], ['27-код-ревью', 'Пустой третий столбец — находка Medium, а не примечание'], - ['27-код-ревью', '«Тест умеет падать» без названной мутации и её вывода доказательством не является'], + ['27-код-ревью', '«Тест умеет падать» без названной мутации доказательством не является'], ['27-код-ревью', 'Вердикт привязан к SHA (#312)'], ['27-код-ревью', 'новое имя в нём — находка ревью, а не запись в список'], ['8-гейты', 'ревьюер обязан перечислить, какие гейты прогнал, какие нет и почему'], diff --git a/test/process-track.test.mjs b/test/process-track.test.mjs index 5d37d159..0ef5c931 100644 --- a/test/process-track.test.mjs +++ b/test/process-track.test.mjs @@ -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/);