mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
210 lines
16 KiB
Markdown
210 lines
16 KiB
Markdown
# ТЗ #422 — Гейт съёмки меряет между прогонами, гейт якорей — достижимость
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/422
|
||
- Приоритет: P3, infra; полный трек — две несвязанные поверхности (съёмка
|
||
документации и гейт ревью-документа), критерий «одна поверхность» для `small`
|
||
не выполняется. Класс файлов — только B (`.github/**`, `demo/**`,
|
||
`scripts/**`, `test/**`), продуктового кода задача не касается; прецеденты
|
||
инфраструктурных задач с файлом ТЗ — #398, #399, #404
|
||
- Ревизия: 1 (2026-09-02)
|
||
|
||
## Сценарий
|
||
|
||
Две проверки, каждая заведена после конкретного инцидента, и каждая на своём же
|
||
инциденте промолчала бы.
|
||
|
||
Первая: после #410 в конвейер добавлен шаг «Кадр не плавает внутри одного
|
||
состояния». Он делает три снимка подряд в одном процессе. Дефект #410 был не
|
||
такой: обрезка кадра плавала между прогонами, и три снимка внутри одного
|
||
процесса совпали бы всегда.
|
||
|
||
Вторая: после #413 гейт отказывает ревью-документу, объявившему материал на
|
||
несуществующем коммите. После #414 отказ смягчается, если в машинном блоке есть
|
||
живые якоря — дерево и блоб, которые ребейз не меняет. Живость якоря проверяется
|
||
`git cat-file -e`, то есть наличием объекта в локальной базе. Объект, созданный
|
||
на машине автора и никуда не привязанный, этой проверке удовлетворяет.
|
||
|
||
## Что человек увидит до и после
|
||
|
||
Видимого поведения продукта задача не меняет — меняется то, что видит инженер.
|
||
|
||
**До**: (1) съёмка может стать недетерминированной между прогонами, а гейт
|
||
останется зелёным; (2) отказ «материал ревью недостижим» смягчается объектом,
|
||
существующим только на машине, где ошибку и совершили.
|
||
**После**: недетерминированность съёмки между прогонами роняет конвейер;
|
||
якорь считается живым только если он достижим из `refs/remotes/origin` или
|
||
тегов — то есть если команда, которую документ печатает читателю, действительно
|
||
находит материал.
|
||
|
||
## Проблема и контракты по пунктам
|
||
|
||
### (1) Гейт стабильности съёмки
|
||
|
||
`demo/docs/capture.mjs:290-312` в режиме `--stability=N` делает N снимков в
|
||
одном процессе без правки состояния и сравнивает их попиксельно.
|
||
`.github/workflows/docs-screenshots.yml:99-100` вызывает его с `--stability=3`.
|
||
|
||
**Воспроизведено исполнением.** В `capture.mjs` внесена мутация, точно
|
||
моделирующая #410: смещение обрезки на 1 px, постоянное внутри процесса и
|
||
разное между прогонами.
|
||
|
||
```
|
||
node demo/docs/capture.mjs --stability=3 → «кадр не плавает ни в одном сценарии»
|
||
прогон A: 6ba168494519a10dd659
|
||
прогон Б: 4142a6ab9558cb481ecc ← 08-room-card.png разошёлся
|
||
```
|
||
|
||
Шаг «Хеши кадров» (`:101-103`) печатает `sha256sum` в лог, но ничего не
|
||
сравнивает: это материал для человека, а не гейт.
|
||
|
||
**Контракт**: воспроизводимость съёмки проверяется там, где она нарушалась —
|
||
**между независимыми прогонами**. Два полных запуска `capture.mjs` в разных
|
||
процессах на одном и том же коммите обязаны дать побайтово совпадающие кадры;
|
||
расхождение роняет конвейер и называет разошедшиеся файлы.
|
||
|
||
Проверка внутри одного состояния при этом **остаётся**: она отвечает на другой
|
||
вопрос — «плавает ли кадр от времени внутри страницы» — и стоит дёшево. Одна
|
||
не заменяет другую; вместе они покрывают обе оси, по которым съёмка может
|
||
поехать.
|
||
|
||
**Отдельно — корневая правка #410**: целочисленная обрезка (`capture.mjs:284-289`)
|
||
не покрыта ничем. Выражение чистое (вход — прямоугольник с дробями, выход —
|
||
целый), значит проверяется юнитом без браузера.
|
||
|
||
### (2) Живость якорей материала
|
||
|
||
`scripts/review-doc-guard.mjs:305-307`:
|
||
|
||
```js
|
||
const resolveObjects = (object) => spawnSync('git', ['cat-file', '-e', object], …).status === 0;
|
||
```
|
||
|
||
`git cat-file -e` истинен для любого объекта в локальной базе, включая
|
||
недостижимый и обречённый на `gc`. Для SHA раунда достижимость намеренно
|
||
считается от `refs/remotes/origin` и тегов (`:298-304`), для якорей — нет.
|
||
|
||
**Воспроизведено исполнением**:
|
||
|
||
```
|
||
объект 8ce20a2a0a8b… (git hash-object -w, ни к чему не привязан)
|
||
git cat-file -e: признаёт живым
|
||
достижим из origin/тегов: НЕТ
|
||
git log --all --find-object: не находит
|
||
danglingMaterialRefusal(недостижимый SHA + этот якорь) → WARNING, отказ смягчён
|
||
```
|
||
|
||
**Контракт**: якорь считается живым тогда и только тогда, когда его находит
|
||
команда, которую документ печатает читателю (`materialAnchorBlock`), выполненная
|
||
в области `refs/remotes/origin` и тегов. Для дерева это перебор `%T` по тем же
|
||
ссылкам, для блоба — `--find-object` по ним же; тип объекта различается
|
||
`git cat-file -t`. Область `--all` не годится: она включает локальные ветки, а
|
||
их у ревьюера нет.
|
||
|
||
**Граница**: гейт по-прежнему судит момент публикации, а не будущее ветки
|
||
(§«Чего этот рубеж НЕ умеет» в самом файле). Задача не меняет этой границы —
|
||
она только приводит проверку якоря к той же строгости, что и проверка SHA.
|
||
|
||
## Скоуп / не-скоуп
|
||
|
||
**В скоупе**: кросс-прогонная проверка съёмки (`capture.mjs`, шаг конвейера),
|
||
юнит на целочисленную обрезку, проверка достижимости якорей в
|
||
`review-doc-guard.mjs`, тесты и мутанты, комментарий о порядке шагов в
|
||
`docs-screenshots.yml`.
|
||
|
||
**Не в скоупе**: состав сценариев съёмки и их фикстуры; порог кадров-свидетелей
|
||
(#408/#409 — закрыты); формат машинного блока якорей (#414); правило «SHA
|
||
раунда обязан быть достижим» (#413) — оно не меняется; три проверки, не умеющие
|
||
падать (#421) — соседний класс, своя задача.
|
||
|
||
## UX, модель данных, i18n
|
||
|
||
Не применимо: продуктового кода задача не касается, пользовательских строк не
|
||
добавляет.
|
||
|
||
## Критерии приёмки
|
||
|
||
- **AC1**. Съёмка, недетерминированная **между прогонами**, роняет конвейер:
|
||
два независимых запуска `capture.mjs` на одном коммите сравниваются побайтово,
|
||
расхождение печатает имена файлов и завершается ненулевым кодом.
|
||
Доказательство: прогон на коде с мутацией, моделирующей #410 (смещение
|
||
обрезки, постоянное в процессе и разное между прогонами), — гейт красный.
|
||
- **AC2**. **Отрицательный прогон обязателен**: та же мутация при старом гейте
|
||
(`--stability=3`) остаётся зелёной. Доказательство: мутант в
|
||
`scripts/mutation-gate.mjs`, прогнанный штатным раннером.
|
||
- **AC3**. Проверка «кадр не плавает внутри одного состояния» сохранена и
|
||
по-прежнему краснеет на нестабильности внутри процесса. Доказательство:
|
||
существующий режим `--stability` не тронут в части сравнения.
|
||
- **AC4**. Целочисленная обрезка покрыта юнитом: дробный прямоугольник
|
||
расширяется до целого, цель не теряет ни полпикселя по краю.
|
||
Доказательство: тест в `test/`, краснеющий при замене `Math.ceil`/`Math.floor`
|
||
на округление к ближайшему.
|
||
- **AC5**. Якорь, существующий только локально, **не** смягчает отказ:
|
||
`danglingMaterialRefusal` с недостижимым SHA и таким якорем возвращает отказ,
|
||
а не warning. Доказательство: тест с подставным `resolveObjects`, отражающим
|
||
достижимость, а не наличие.
|
||
- **AC6**. Достижимый якорь по-прежнему смягчает отказ до warning — поведение
|
||
#414 не сломано. Доказательство: тот же тест, вторая ветка; существующие
|
||
тесты `review-doc-guard` зелёные без правок их утверждений.
|
||
- **AC7**. Дерево и блоб проверяются каждый своей командой, тип определяется
|
||
`git cat-file -t`; неизвестный тип объекта живым не считается.
|
||
Доказательство: юнит на трёх входах.
|
||
- **AC8**. Конвейер не стал заметно дольше: второй прогон съёмки — это ещё один
|
||
`capture.mjs` на том же браузере. Доказательство: замер до и после, разница
|
||
названа в issue числом.
|
||
- **AC9**. Комментарий о порядке шагов в `docs-screenshots.yml` соответствует
|
||
фактическому порядку. Доказательство: чтение файла — комментарий и порядок
|
||
шагов сверяются глазами в одном экране.
|
||
|
||
## План автотестов
|
||
|
||
**Юниты** (`test/`):
|
||
|
||
1. `capture-clip.test.mjs` — целочисленная обрезка на дробных входах (AC4).
|
||
2. `review-doc-guard.test.mjs` (дополнение) — якорь недостижим → отказ; достижим
|
||
→ warning; неизвестный тип → не живой (AC5, AC6, AC7).
|
||
|
||
**Гейт конвейера**: шаг, запускающий съёмку дважды и сравнивающий хеши всех
|
||
кадров (AC1). Локально та же проверка доступна командой, названной в
|
||
`docs/TESTING.md`.
|
||
|
||
**Мутанты** (`scripts/mutation-gate.mjs`):
|
||
|
||
- `capture-drifts-between-runs`: смещение обрезки, постоянное в процессе и
|
||
разное между прогонами → новый кросс-прогонный гейт красный, старый
|
||
`--stability` зелёный (AC1, AC2);
|
||
- `anchor-liveness-ignores-reachability`: вернуть `git cat-file -e` → тест AC5
|
||
красный.
|
||
|
||
## Риски
|
||
|
||
- **Второй прогон съёмки удваивает время шага.** Съёмка десяти сценариев — не
|
||
самая долгая часть конвейера, но и не бесплатная. Смягчение: AC8 требует
|
||
замера, а не предположения; если удвоение окажется дорогим, второй прогон
|
||
можно свести к тем сценариям, где обрезка нетривиальна (`room-card` и
|
||
соседи) — но тогда это должно быть записано, а не подразумеваться.
|
||
- **Кросс-прогонное сравнение поймает не только нашу недетерминированность.**
|
||
Обновление Chromium в кэше между двумя прогонами одного job'а невозможно, но
|
||
системный шрифтовой кэш и подобное — теоретически да. Смягчение: оба прогона
|
||
идут в одном job'е, на одном браузере и одном коммите; если гейт начнёт
|
||
краснеть по внешней причине, это само по себе важная новость.
|
||
- **Проверка достижимости якорей дороже `cat-file -e`.** Перебор `%T` по
|
||
ссылкам origin — это `git log` по всей истории. Смягчение: вызывается только
|
||
в редкой ветке (SHA раунда уже недостижим), то есть на отказе, а не на каждом
|
||
прогоне.
|
||
- **Ужесточение якорей сделает часть прошлых отчётов формально негодными.**
|
||
Это правда и это цель: смягчение, которое не воспроизводится, хуже отсутствия
|
||
смягчения. Смягчение риска: сообщение отказа обязано называть команду, которой
|
||
ревьюер может проверить якорь сам.
|
||
|
||
## Откат
|
||
|
||
Обе правки локальны и независимы: шаг конвейера снимается отдельно от проверки
|
||
достижимости. Данные пользователя и продуктовый код не затрагиваются.
|
||
|
||
## Release-артефакты
|
||
|
||
- `docs/CHANGELOG.md` / `docs/CHANGELOG.ru.md`: **не требуется**
|
||
(User-Visible: no).
|
||
- `docs/TESTING.md`: команда локального кросс-прогонного сравнения съёмки.
|
||
- Скриншоты не меняются: задача проверяет съёмку, а не кадры.
|