mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
docs: specify the capture and anchor gates (#422)
User-Visible: no Issue: #422
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
# ТЗ #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`: команда локального кросс-прогонного сравнения съёмки.
|
||||
- Скриншоты не меняются: задача проверяет съёмку, а не кадры.
|
||||
Reference in New Issue
Block a user