fix(gates): съёмка кадров в чужой среде отказывается заранее

Вопрос владельца: зачем агенты снимают PNG на Windows, если кадры мы всё
равно не принимаем, тем более что WSL есть на обеих машинах. Ответ по
коду: этому ничто не мешало. Ни один из шести скриптов съёмки и приёмки
не знал, на какой он ОС — ни `process.platform`, ни win32, ни WSL не
упоминались нигде. Съёмка отрабатывала штатно, а стена появлялась на
приёмке, и текст стены говорил «сцен-свидетелей 0 из 10», то есть
подсказывал неверный вывод «надо объявить больше сцен» — от которого до
`--expect-change` на всю матрицу одна команда.

Причина запрета не политика, а физика: Windows растеризует текст через
DirectWrite, с другим субпиксельным сглаживанием и DPI, поэтому
байтового совпадения с принятым эталоном не даёт никогда, свидетелей
среды быть не может, и приёмка откажет всё равно. Флаги детерминизма из
#410 убирают разброс внутри среды, а не между ОС.

Что сделано:

- `golden:capture` отказывается до запуска браузера, в тексте отказа
  готовая команда для WSL;
- `golden:verify` остаётся законным в любой среде: он ничего не
  принимает, а как грубая проверка полезен;
- обе приёмки (golden и документации) отказываются в чужой среде;
- к отказу свидетелей приписывается фраза про расхождение среды — та
  самая, которой не хватало, чтобы отказ не читался как «объяви больше
  сцен»;
- платформа уезжает в манифесты рядом с версией Chromium: у кадров
  появился провенанс среды;
- осознанный обход есть и требует причину: HP_ALLOW_FOREIGN_CAPTURE.

Главный урок задачи — про цену правки файла, у которого записан хеш.
Первая редакция встроила проверку в `demo/docs/capture.mjs`, и гейт
документации сразу покраснел: его sha записан в индексе скриншотов, и
`scripts/check-docs.mjs` их сверяет. То есть проверка, которая ничего не
рисует, стоила бы пересъёмки всех картинок документации и визуальной
приёмки владельца. Поэтому для документации отказ живёт шагом раньше —
`npm run docs:capture` вызывает `scripts/assert-capture-env.mjs` — и
шагом позже, на приёмке. По той же причине гейт golden стоит в
`demo/golden/policy.mjs`, а не в `run.mjs`: последний входит в корпус
sourceFingerprint. Тест закрепляет обе границы: гейт обязан быть в
policy.mjs и в npm-скрипте и обязан отсутствовать в двух
фингерпринтуемых файлах.

Платформа в юнитах — параметр, а не `process.platform`: иначе тест был
бы зелёным на Linux и красным на машине владельца, то есть тестом про
хост, а не про правило.

AGENTS.md приведён к состоянию после #401 (принимается любая среда,
доказавшая себя байтовым совпадением непринятых кадров) и разводит
проверку и съёмку — прежний текст сливал их в «advisory» и утверждал
«accepted only on a complete Linux CI artefact».

Свидетели, все проверены отрицательным прогоном: снятый гейт съёмки,
гейт, отказывающий и на verify, обход без причины, отказ без команды,
убранная приписка про среду, отцепленные гейты обеих приёмок,
переставшая бросать обёртка, npm-скрипт без проверки и возврат гейта в
каждый из двух фингерпринтуемых файлов. Проверка подключения сначала
смотрела только на импорт модуля и молча проходила, когда отказ
заменяли на `void` — теперь она проверяет вызов бросающей обёртки.

Гейты: npm test 1918 tests, 1917 pass, 0 fail; typecheck зелёный;
check-docs зелёный (индекс скриншотов не задет); pytest без HA 378
passed, 3 skipped.

Issue: #455
User-Visible: no
This commit is contained in:
Claude
2026-09-04 19:26:35 +03:00
parent 9e56842bc1
commit 28a4cb5eec
10 changed files with 397 additions and 6 deletions
+13
View File
@@ -25,6 +25,7 @@
*/
import { createHash } from 'node:crypto';
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
import { assertCaptureEnvironment, captureEnvironment } from './capture-environment.mjs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -94,9 +95,13 @@ export function verifyDocsCandidate({
/** Build the manifest written by the CLI without erasing an earlier review trace. */
export function acceptedDocsManifest({
manifest, previousAcceptance, decision, skipWitnesses = false, skipReason = '',
platform = null,
}) {
return {
...manifest,
// Провенанс среды приёмки (#455): рядом с версией браузера, чтобы у
// следующего разбора «почему кадры разошлись» была не только догадка.
...(platform ? { acceptedOn: platform } : {}),
acceptance: decision.replace.length
? {
declared: [...decision.replace],
@@ -114,6 +119,13 @@ const list = (argv, name) => argv
.filter(Boolean);
function main(argv) {
// Среда приёмки (#455). Платформу съёмки манифест не несёт и не может:
// добавить поле — значит править `demo/docs/capture.mjs`, чей sha записан в
// индексе скриншотов, то есть платить пересъёмкой десяти картинок за
// проверку. Приёмка идёт там, где лежат артефакты, поэтому её платформа —
// достаточный признак среды кадров.
const allowed = assertCaptureEnvironment({ kind: 'docs', stage: 'accept' });
if (allowed) console.log(`Чужая среда приёмки разрешена осознанно: ${allowed}`);
if (!argv.includes('--reviewed')) {
console.error('отказ: замена скриншотов без явного --reviewed');
return 2;
@@ -166,6 +178,7 @@ function main(argv) {
manifest,
previousAcceptance: previous,
decision,
platform: captureEnvironment().platform,
skipWitnesses,
skipReason,
});