mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
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:
@@ -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 }),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
}],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user