docs: review document for #514

Issue: #514
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-09 21:18:51 +00:00
parent 0796a01571
commit 39571e20d6
+135
View File
@@ -0,0 +1,135 @@
# CODE-REVIEW-514-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/514 — «E2E на реальном HA как гейт стабильного релиза»
- **ТЗ:** `docs/specs/514-e2e-stable-release-gate.md`, ревью ТЗ зелёное (`docs/reviews/SPEC-REVIEW-514-r1.md`, r1, High 0/Medium 0)
- **Материал ревью:** `395b0e38f4b3e34071e2bcf236440bf476b2c75a` (рабочая копия на нём, HEAD detached from `52124529`)
- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4
- **Вердикт: зелёный**
## Скоуп ревью
Диапазон `origin/dev..HEAD` — 5 коммитов, ровно issue #514 (`fd4f1077` ТЗ, `e226ac56` документ ревью ТЗ, `9b9e306b`/`5e9fe662`/`395b0e38` реализация и две правки по живым прогонам). Файлы только класса B (`scripts/**`, `test/**`, `.github/workflows/release.yml`) и C (`docs/**`); класс A не задет — `src/**`, `custom_components/**` не тронуты. `User-Visible: no` на всех пяти коммитах, что верно: изменение не видно ни пользователю HACS, ни пользователю карточки — только владельцу в логе гейта.
Проверялись все 6 AC ТЗ, три заявленных мутанта, соответствие `houseplan-e2e/e2e.yml` контракту, который `scripts/e2e-gate.mjs` полагает существующим, и трейлеры/процесс-гейт.
## Как проверялось
Материал ревью — код на точном SHA `395b0e38`, без git fetch/pull/checkout на другой коммит.
1. Прочитан `scripts/e2e-gate.mjs` целиком (149 строк): `previousStable`, `isOurRun`, `classifyRun`, `e2eGate`, `realOps`, CLI-обвязка.
2. Прочитаны `test/e2e-gate.test.mjs` (12 тестов) и `test/release-workflow.test.mjs` (2 теста) — сопоставлены с AC1–AC3, AC5.
3. Прочитан диф `.github/workflows/release.yml` — положение нового шага, условие, токен.
4. Прочитаны три новых определения в `scripts/mutation-gate.mjs` (`release-ships-on-red-e2e`, `release-upgrades-stable-onto-itself`, `release-trusts-foreign-e2e-run`).
5. Прочитан диф `PROCESS.md`, `AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, `docs/specs/README.md` — сверены с фактическим положением вставки (номера строк из ТЗ и хендоффа совпали).
6. **`houseplan-e2e/.github/workflows/e2e.yml` получен напрямую** (`gh api repos/Matysh/houseplan-e2e/contents/...`), не со слов хендоффа: подтверждены job `plan` (собирает матрицу, `journeys-dev` только при `schedule`), job `e2e` с именем `"${{ matrix.suite }} · HP ${{ matrix.ref }} · HA ${{ matrix.ha }}"`, `concurrency.cancel-in-progress: false`.
7. **Оба живых прогона, названных в хендоффе, проверены напрямую через `gh api`**, а не приняты на слово:
- `gh api repos/Matysh/houseplan-e2e/actions/runs/34393136097/jobs` → `Матрица прогона`, `journeys · HP v1.73.0 · HA stable`, `first-run · HP v1.73.0 · HA stable`, `upgrade · HP v1.72.0 · HA stable` (conclusion всех `success`) — подтверждает AC4 (3 сьюта, без `journeys-dev`) и обоснованность правки в `isOurRun` (job `upgrade` несёт тег **предыдущего** stable, не тег под тестом).
- `gh api repos/Matysh/houseplan-e2e/actions/runs/34392391382/jobs` → `upgrade · HP stable · HA stable` = `failure` — подтверждает заявленную причину появления `previousStable` (обновление v1.73.0 на себя же).
- `gh release list --repo Matysh/houseplan-card --json tagName,createdAt,isPrerelease` — подтверждает допущение `previousStable`, что `gh release list` отдаёт новые релизы первыми (реализация полагается на этот порядок без явной сортировки).
8. Прогнаны тесты и мутанты (список — в разделе «Гейты»).
9. `node scripts/process-gate.mjs --range origin/dev..HEAD` — «гейт пройден, предупреждений 0».
## Гейты — что прогнано и почему
| Гейт | Результат | Основание |
|---|---|---|
| `npx tsc --noEmit`, `npm test`, `npm run build` + сверка бандла | не перегонялись | Validate зелёный на этом точном SHA (`395b0e38`, https://github.com/Matysh/houseplan-card/actions/runs/34394268282) — сошлись на этом прогоне |
| `node --test test/e2e-gate.test.mjs` | **12/12 green**, прогнан лично | новый файл, доказывает AC1/AC2 |
| `node --test test/release-workflow.test.mjs` | **2/2 green**, прогнан лично | новый файл, доказывает AC1/AC3 |
| `node --test test/release-contract.test.mjs test/performance-workflow.test.mjs test/validate-workflow.test.mjs` | **34/34 green**, прогнан лично | регресс-проверка: новый шаг в `release.yml` не сломал соседние тесты по этому же файлу |
| `node scripts/mutation-gate.mjs --id=release-ships-on-red-e2e` | **поймано 1 из 1** | защитный AC1/AC5 — гейт не должен читать `failure` как `green` |
| `node scripts/mutation-gate.mjs --id=release-upgrades-stable-onto-itself` | **поймано 1 из 1** | защитный AC5 — `upgrade_from` не должен всегда быть `stable` |
| `node scripts/mutation-gate.mjs --id=release-trusts-foreign-e2e-run` | **поймано 1 из 1** | защитный AC2/AC5 — гейт не должен принимать чужой dispatch |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «исполняемого frontend-диффа нет … выбирать нечего» | `src/**/*.ts` не тронут — смоки проверяют собранную карточку, её нет в этом диффе |
| `npm run golden:verify` | не прогонялся | diff не меняет рендер/визуал (нет `src/**`) |
| `node scripts/check-docs.mjs` | не прогонялся | diff не трогает `src/**` — условие «прогонять» не выполнено |
| `node scripts/model-invariants.mjs` | не прогонялся | геометрия/`layout`/толщина/`marker.space` не затронуты |
| `python -m pytest tests_backend -q` | не прогонялся | `custom_components/**/*.py` не тронут |
| `node scripts/process-gate.mjs --range origin/dev..HEAD` | **«гейт пройден, предупреждений 0»**, прогнан лично | трейлеры/имя ветки/докладка `small` |
Полный список смоков (`ls demo/smoke_*.mjs | wc -l`) не запрашивался — инструмент выбора уже прямо ответил «выбирать нечего», уточнять число бессмысленно.
## Защитные AC — таблица «чем краснеет» (§2.7)
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| AC1 (красный/отсутствующий/отменённый/ошибка dispatch → гейт красный) | `test/e2e-gate.test.mjs` (7 тестов), `test/release-workflow.test.mjs` | мутант `release-ships-on-red-e2e` (снята проверка `conclusion === 'success'`) — поймано 1/1, прогнано лично |
| AC2 (опознание своего прогона, игнор чужого) | `test/e2e-gate.test.mjs` (4 теста), живой прогон 34393136097 (v1.72.0-гейт не принял бы run v1.73.0) | мутант `release-trusts-foreign-e2e-run` (`isOurRun` всегда `true`) — поймано 1/1, прогнано лично |
| AC3 (пре-релизы шаг не выполняют) | `test/release-workflow.test.mjs` (regex на `if:`) | чтением: единственная точка контроля — `if: ${{ !github.event.release.prerelease }}` в самом yml, тест сверяет буквальную строку; отдельного мутанта на YAML-условие в репозитории для соседних гейтов тоже нет (см. `Require full performance`) |
| AC4 (`journeys-dev` не бежит на dispatch) | коммит в `houseplan-e2e` + живой прогон 34393136097 (3 job, без `journeys-dev`) | чтением + живым прогоном: `job.plan` собирает матрицу и добавляет `journeys-dev` только `if (schedule)` — код прочитан напрямую через `gh api`, не со слов хендоффа; мутанта нет, но у `houseplan-e2e` нет собственного процесса код-ревью с мутантами (внешний репозиторий) |
| AC5 (все три мутанта пойманы) | штатный `mutation-gate.mjs --id=…` | прогнано лично, все три «поймано 1 из 1» |
| AC6 (документация, `User-Visible: no`) | чтением дифа PROCESS.md/AGENTS.md/DEVELOPMENT.md/TESTING.md/specs/README.md + трейлеры коммитов | не защитный AC (расположение/текст) — таблица не требуется по исключению §2.7 |
## Находки
Не найдено ни одной High- или Medium-находки.
**Low (снимаю без правки — не искажает ни один AC, не создаёт риска пропуска красного гейта):**
- **L1.** Если job `plan` в `e2e.yml` сама завершится с `failure` до того, как заведётся хотя бы одна job вида `· HP … ·` (например, транзиентная ошибка в тривиальном `node -e`-скрипте планирования матрицы), `classifyRun` вернёт `'unknown'`, а поскольку прогон уже `completed`, код `e2eGate` (строка `if (kind === 'foreign' || candidate.status === 'completed') foreign.add(...)`) навсегда добавит его в `foreign` и продолжит ждать. Результат — `missing` через `appearMs` вместо диагностичного «наш прогон упал на планировании матрицы». Инвариант «не публиковать ассеты на красном» не нарушается: `missing` — это тоже небезопасный (не-green) результат, шаг `release.yml` падает, `build` не выполняется. Единственная цена — менее понятное сообщение владельцу в этом маловероятном сценарии (сам скрипт планирования — детерминированный `node -e` без внешних вызовов). Не блокирует, вне рамок AC (AC1 требует «красный/отсутствующий/… → гейт красный с причиной», а не конкретный текст причины для этого под-случая).
- **L2 (унаследовано из ревью ТЗ, переносится без повторной проверки).** Таймаут `totalMs = 45 мин` взят из `merge-candidate.mjs`, а не выведен из `timeout-minutes: 40` самих job `e2e.yml` — даёт 5 минут запаса, чего достаточно (уже отмечено ревью ТЗ как L2, снято без правки).
## Проверено чтением и/или исполнением — по каждому файлу
- `scripts/e2e-gate.mjs` — прочитан целиком, логика прослежена вручную по всем ветвям (green/red/missing/cancelled/error, чужой/свой прогон, ещё не решённый прогон) и подтверждена тестами + мутантами.
- `test/e2e-gate.test.mjs`, `test/release-workflow.test.mjs` — прогнаны лично, зелёные; содержание тестов сверено построчно с тем, что они утверждают доказать (не только «зелёный», но и что именно проверяет каждый `assert`).
- `.github/workflows/release.yml` — прочитан целиком в контексте (job `gate`/`build`, `needs: gate`), положение и условие нового шага верны; убедился, что `build` не может выполниться при падении шага (через `needs: gate`).
- `scripts/mutation-gate.mjs` — три новых определения прочитаны, мутации осмысленны (реально снимают заявленную защиту, а не косметическую строку), прогнаны лично — все поймано.
- `houseplan-e2e/.github/workflows/e2e.yml` — получен напрямую с GitHub, не из пересказа хендоффа; job `plan`/`e2e` прочитаны целиком.
- Живые прогоны `34393136097` и `34392391382` — сверены напрямую через `gh api`, не приняты на слово хендоффа.
- `PROCESS.md`, `AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, `docs/specs/README.md` — прочитаны в диффе, соответствуют факту (проверено предыдущими пунктами: положение шага, имя job, токен, поведение `journeys-dev`).
- Трейлеры всех 5 коммитов (`Issue: #514`, `User-Visible: no`) — проверены `git show -s --format=full`, корректны.
- `node scripts/process-gate.mjs --range origin/dev..HEAD` — «гейт пройден, предупреждений 0».
## Чего не проверял
- `tsc`/`npm test` (полный)/`npm run build` — не перегонял, полагаюсь на зелёный Validate этого точного SHA (ссылка выше), как разрешает правило «дешёвые гейты уже подтверждены».
- Реальную работоспособность `HP_PROCESS_TOKEN`/`E2E_DISPATCH_TOKEN` на `workflow_dispatch` в `houseplan-e2e` при событии `release: published` — недоступно ревьюеру (нет прав на секреты, событие `release` нельзя сэмулировать без публикации настоящего релиза). ТЗ и хендофф это честно называют «первая живая проверка — следующий stable»; путь фейл-safe (403 → красный шаг с текстом про секрет, что покрыто тестом `#514 AC1: a dispatch refused by the token…`).
- Полный набор мутантов `mutation-gate.mjs` (весь файл, 8000+ строк) — прогонял только три новых по `--id`, не весь `--only`/полный корпус: задача добавляет ровно три мутанта, остальные к диффу не относятся и это предрелизный, а не ревью-гейт.
- `golden:verify`, `check-docs.mjs`, `model-invariants.mjs`, `pytest tests_backend` — не прогонялись: diff не затрагивает `src/**`, геометрию или `custom_components/**/*.py` (см. таблицу гейтов и обоснование там же).
- Полный список `demo/smoke_*.mjs` через `wc -l` не запрашивал — `smoke-select.mjs` прямо ответил «выбирать нечего».
## Что проверено и корректно
- Все 6 AC ТЗ пронумерованы и выполнены, каждый доказан тестом/мутантом/чтением с указанием, чем именно.
- Три заявленных мутанта реально снимают заявленную защиту и пойманы штатным раннером (1 из 1 каждый), прогнано лично, не принято на слово хендоффа.
- Контракт имени job в `houseplan-e2e/e2e.yml` (`"${suite} · HP ${ref} · HA ${ha}"`), на который опирается `isOurRun`, подтверждён прямым чтением workflow чужого репозитория, а не пересказом.
- Два живых прогона, на которые ссылается хендофф, подтверждены напрямую через `gh api` — job-состав и `conclusion` совпадают с заявленным.
- `previousStable` полагается на порядок `gh release list` (новые первыми) — порядок подтверждён прямым вызовом на реальном репозитории, а не одним лишь тестом с придуманным порядком входа.
- Трейлеры, имя ветки, `User-Visible: no` + отсутствие изменений в changelog (верно — no user-visible изменение) — корректны, `process-gate.mjs` подтверждает.
- Диапазон изменений строго в рамках скоупа ТЗ (§2): `scripts/e2e-gate.mjs`, `release.yml`, `mutation-gate.mjs`, два новых тестовых файла, документация. Скоуп не расширен, попутных правок «раз уж я здесь» не обнаружено.
- Одно число/один источник — неприменимо: диф не добавляет и не меняет ни одной пользователем видимой величины.
## Продуктовое рассуждение
Задача закрывает не строку `docs/SCOPE.md` напрямую (это инфраструктура процесса релиза, не поверхность продукта), а укрепляет процесс, которым продукт поставляется в HACS: раньше `houseplan-e2e` существовал и был зелёным, но stable-релиз уходил пользователям, не дождавшись его. Теперь дождётся. Это ровно то, что заявлено в issue и ТЗ, без расширения и без ухудшения смежного поведения (беты не тронуты, что подтверждено условием `if` и тестом AC3).
## Вопросы владельцу
Нет. Открытых продуктовых вопросов не осталось — единственный технический вопрос из тела issue (нужен ли `E2E_DISPATCH_TOKEN`) снят автором фолбэком на уровне кода, реализация фейл-safe при отсутствии прав.
---
## Материал раунда
- SHA материала: `395b0e38f4b3e34071e2bcf236440bf476b2c75a`
- Диапазон: `origin/dev..HEAD` (5 коммитов)
- ТЗ: `docs/specs/514-e2e-stable-release-gate.md`, ревью ТЗ: `docs/reviews/SPEC-REVIEW-514-r1.md` (зелёное, r1)
- Заход: r1 · это первый заход код-ревью, унаследовать нечего
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/514-e2e-stable-release-gate`, коммит `5212452994ce` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `47cb0575e5e31df27eb1d4c56e109a440a23011f`
```
git log --all --format='%H %T' | grep 47cb0575e5e3
```
- ТЗ `docs/specs/514-e2e-stable-release-gate.md`, блоб `30795778fee11c8df610440dd86b745112e120c4`
```
git log --all --find-object=30795778fee11c8df610440dd86b745112e120c4 -- docs/specs/514-e2e-stable-release-gate.md
```
- Вердикт конвейера: `green` · High 0