/** * Правило допустимости съёмки golden-кандидатов (#334). * * Прежде эталон принимался только из артефакта CI: растеризация шрифтов на * другой машине может отличаться, а доказать обратное было нечем. Цена — * двойной roundtrip на каждый визуальный фикс: пуш, семь минут CI, скачивание * артефакта, приёмка, второй пуш, второй полный прогон. При версии матрицы 48 * это платится регулярно. * * Доказательство есть, и оно эмпирическое: среда съёмки равна раннеру, если * КАЖДАЯ сцена, которую менять не собирались, совпала со своим принятым * эталоном. Расхождение растеризации нельзя спрятать — оно задевает все сцены * с текстом, а не только правленные. Поэтому ревьюер объявляет намерение * списком `--expect-change`, и всё, что разошлось помимо списка, приёмку * запрещает: либо это регрессия рендера, либо среда не та. * * То же правило ловит другое, независимо от среды: «принять всё, чтобы CI * позеленел». Именно так эталон перестаёт быть эталоном — молча, одной * командой, без единого названного намерения. * * `missing-baseline` объявления не требует: у новой сцены нет эталона, которому * она могла бы противоречить. * * Почему это лежит в `scripts/`, а не рядом с `accept.mjs`. Отпечаток * `sourceFingerprint` включает ВСЕ `.mjs` из `demo/golden`, включая * `accept.mjs` и `policy.mjs`, которые исполняются после того, как картинка уже * снята, и ни одного пикселя изменить не могут. Правка любого из них объявляет * устаревшими и закоммиченный бандл, и манифест скриншотов документации, и * манифест эталонов — то есть требует ровно того двойного цикла, который эта * задача убирает. Сужение корпуса отпечатка — отдельная задача: оно неизбежно * требует пересборки бандла, потому что сам `source-fingerprint.mjs` в корпусе. */ export const goldenAcceptanceRefusal = (results, declared = []) => { if (!Array.isArray(results)) return 'отчёт кандидатов не содержит результатов сцен'; const expected = new Set(declared.filter(Boolean)); const known = new Set(results.map((result) => result.id)); const unknown = [...expected].filter((id) => !known.has(id)); if (unknown.length) { return `в --expect-change названы сцены, которых нет в отчёте: ${unknown.sort().join(', ')}`; } const undeclared = results .filter((result) => result.status === 'different' && !expected.has(result.id)) .map((result) => result.id) .sort(); if (undeclared.length) { return 'съёмка разошлась с принятыми эталонами в сценах, которые менять не собирались:' + ` ${undeclared.join(', ')}.` + ' Либо это регрессия рендера, либо среда съёмки не совпадает с раннером —' + ' в обоих случаях приёмка запрещена. Намеренные сцены перечисляются в' + ' --expect-change='; } return null; }; /** Сцены, объявленные изменёнными, но совпавшие с эталоном: не ошибка, но и не молчание. */ export const goldenSilentDeclarations = (results, declared = []) => declared .filter((id) => results.find((result) => result.id === id)?.status === 'passed') .sort();