Files
houseplan-card/docs/reviews/CODE-REVIEW-514-r4.md
T
2026-09-09 21:37:26 +00:00

171 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-514-r4
- **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)
- **Материал ревью:** ровно `0ce5e4eed3d1d2e3a52f07c35dbc9a8297dd01a2` (рабочая копия на нём). `git log --oneline origin/dev..HEAD` — 9 коммитов. `git diff origin/dev...HEAD` — 15 файлов, +1033/−3.
- **Заход:** r4 · блокирующих циклов израсходовано 1 из 4 (израсходован жёлтым вердиктом r3; зелёный вердикт бюджет не тратит, #227)
## Материал предыдущего раунда (SHA не резолвится — обычное дело, §2.10)
r3 (`docs/reviews/CODE-REVIEW-514-r3.md`, вердикт жёлтый, комментарий issue 2026-09-09T21:16:28Z) объявил материал `d166bad3eb293d70cdd37cc715c639449742d988`. Этот объект в текущем репозитории уже не резолвится:
```
git cat-file -t d166bad3eb293d70cdd37cc715c639449742d988
→ fatal: git cat-file: could not get object info
```
Между r3 и этим заходом случился ещё один сдвиг базы (см. комментарий issue 2026-09-09T21:21:59Z: `workflow_sync` увидел, что `main` отстал от `dev` по `process.yml` после слияния #515, конвейер сделал зеркальный коммит `aeb0a473` и пере-опубликовал ветку задачи) — commit-хэши коммитов задачи пересчитались от нового родителя, содержимое не изменилось. Якоря по дереву/блобу, которые пережили бы это (`git log --all --format='%H %T' | grep <дерево>`), r3 не оставил — его собственный ручной блок «Материал раунда» назвал только commit SHA, а машинный блок конвейера в конце документа относится к более раннему, никогда не рецензированному состоянию (`6d28e170`, до второго ребейза). Восстановление сделано по содержимому, не по имени:
```
git diff origin/dev...a8a9b7d9 --stat
→ 14 files changed, 824 insertions(+), 3 deletions(-)
(тот же список файлов и те же числа, что в шапке CODE-REVIEW-514-r3.md)
git merge-base --is-ancestor a8a9b7d9 HEAD && echo yes → yes
git log --oneline a8a9b7d9..HEAD
→ 0ce5e4ee ci: the E2E gate fails loudly when the release list cannot be read
ed6d1d76 docs: review document for #514
```
`a8a9b7d9` — content-эквивалент `d166bad3` (тот же диапазон правок, то же число файлов/строк, тот же список путей), переживший последующий ребейз под другим именем. Это и есть материал, на котором был вынесен вердикт r3; от него считается дельта этого раунда.
**Почему разбор по дельте, а не полный.** База `origin/dev` не продвинулась между r3 и r4 (`git merge-base origin/dev HEAD` и `git merge-base origin/dev a8a9b7d9` — оба `ad2858a8`, один и тот же коммит): ребейза на ушедший вперёд `dev` в этот раз не было, только пересборка ветки поверх той же базы (мираж-коммит `aeb0a473` ушёл в `main`, не в диапазон задачи). Контракт поведения не менялся, новая подсистема не задета, объём дельты (2 коммита, 3 файла, +24/−5 строк) на порядок меньше исходной задачи. Условие «разбор остаётся полным» (§2.9/§2.10) не выполнено ни по одному пункту — сокращённый разбор оправдан.
## Дельта r3 → r4
```
git diff a8a9b7d9..HEAD --stat
```
| Коммит | Класс | Содержимое |
|---|---|---|
| `ed6d1d76` | C (docs) | публикация `docs/reviews/CODE-REVIEW-514-r3.md` конвейером — только новый файл, продуктовых строк нет |
| `0ce5e4ee` | B+C | `scripts/e2e-gate.mjs` (+11/−2), `test/e2e-gate.test.mjs` (+12), `PROCESS.md` (+3/−3) — фикс M1(r3) и формулировки L4(r3) |
Класс A не задет (как и во всём диапазоне `origin/dev..HEAD`); трейлеры `Issue: #514` / `User-Visible: no` на обоих коммитах корректны (`git show -s --format=full`).
## Закрытие раунда r3
| Находка r3 | Чем закрыта | Где это видно |
|---|---|---|
| **M1** (Medium, в скоупе) — фолбэк-токен, scoped только на `houseplan-e2e`, молча ломает `previousStable` через проглоченную ошибку `gh release list` в `releases()`, воспроизводя дефект `4143f998` под чужим диагнозом | `scripts/e2e-gate.mjs`: `releases()` теперь проверяет `r.status !== 0` и бросает `Error` с текстом `gh release list <repo>: <stderr>` — так же, как уже делал `dispatch()`; исключение попадает в тот же `catch` блок `e2eGate` → `result: 'error'` с `TOKEN_HINT`. `TOKEN_HINT` переписан: называет оба репозитория и обе нужные операции («Actions: write на houseplan-e2e И чтение релизов houseplan-card»), а не только первый | `scripts/e2e-gate.mjs:28,80,119-123`; новый тест `test/e2e-gate.test.mjs:117-127` (`#514 r3 M1`) — см. проверку ниже |
| **L4** (Low, новая в r3) — `PROCESS.md:704-706` путал подлежащее и дополнение («E2E … запускает и ждёт `release.yml`», хотя наоборот) | Строка переписана: «`release.yml` сам запускает `e2e.yml` в `houseplan-e2e` на теге и ждёт его зелёного (#514)» | `PROCESS.md:704-706` (diff выше) |
| L1 (унаследована из r1/r2, подтверждена чтением в r3) | Не требовала правки — Low принят без изменений | код `classifyRun`/цикл ожидания в `e2eGate` (строки ~62-97) не тронут дельтой r3→r4, см. «Унаследовано» |
| L2 (унаследована из r1/r2) | Не требовала правки — файл вне материала процесса houseplan-card | не тронут |
| L3 (унаследована из r1/r2) | Не требовала правки — принятый паттерн из `validate-gate.mjs` | не тронут |
**Мутация подтверждает закрытие M1, а не заявление автора.** Временно откатил фикс (вернул `releases()` к варианту «проглотить ошибку», без `throw`) и прогнал новый тест:
```
node --test test/e2e-gate.test.mjs (с откаченным releases())
→ not ok 11 - #514 r3 M1: ...
error: 'Missing expected rejection.'
```
Тест краснеет на снятой защите — «умеет падать» подтверждено, не заявлено. Восстановил `scripts/e2e-gate.mjs` из бэкапа, `git status --short` — пусто, рабочая копия чиста.
С фиксом на месте:
```
node --test test/e2e-gate.test.mjs test/release-workflow.test.mjs
→ # tests 15 # pass 15 # fail 0 (было 14/14 на материале r3 — новый тест M1 прибавил один)
```
| AC / защита | Чем доказан | Чем краснеет |
|---|---|---|
| AC1, ветка отказа `releases()` (M1) | `test/e2e-gate.test.mjs:117-127`, тест `#514 r3 M1` | откат `releases()` к варианту без `throw` → `not ok`, `Missing expected rejection` (прогнано лично, см. выше) |
Мутанты `scripts/mutation-gate.mjs` дельту не затронули (файл не менялся между r3 и r4) — прогнаны все три для очистки:
```
node scripts/mutation-gate.mjs --id=release-ships-on-red-e2e → поймано 1 из 1
node scripts/mutation-gate.mjs --id=release-upgrades-stable-onto-itself → поймано 1 из 1
node scripts/mutation-gate.mjs --id=release-trusts-foreign-e2e-run → поймано 1 из 1
```
## Унаследовано из r3
Без повторной проверки в этом раунде принято (делта их не задевает):
- **AC2** (опознание своего прогона по `HP <tag>`) и **AC4** (`journeys-dev` только по расписанию) — код `classifyRun`/`isOurRun` (`scripts/e2e-gate.mjs:62-97`) и контракт `houseplan-e2e/e2e.yml` дельтой не тронуты; проверены в r3 напрямую (`gh api repos/Matysh/houseplan-e2e/contents/...`), см. `docs/reviews/CODE-REVIEW-514-r3.md`, материал (content-эквивалент) `a8a9b7d9`.
- **AC3** (пре-релизы шаг не выполняют) — `.github/workflows/release.yml` не входит в дельту r3→r4 (`git diff a8a9b7d9..HEAD -- .github/workflows/release.yml` пуст); `test/release-workflow.test.mjs` не менялся и зелёный (см. прогон выше, 2/15 из общего числа).
- **AC5** (три мутанта ловят снятую защиту) — код мутантов не менялся; перепрогнан в этом раунде для очистки (см. таблицу выше), не унаследован голословно.
- **L1** (плановая job `houseplan-e2e/plan` упадёт раньше первой именованной суб-job → `missing`) — строки `scripts/e2e-gate.mjs:62-97`, где живёт этот путь, не входят в дельту `a8a9b7d9..HEAD`. Инвариант «не публиковать на красном» не нарушается.
- **L2** (README `houseplan-e2e` может буквально описывать `upgrade_from=stable`) — внешний файл, вне диапазона `origin/dev...HEAD` в обоих раундах.
- **L3** (окно `gh run list --limit 10`) — код опроса не менялся.
- Порядок `gh release list` (новые первыми), на котором стоит `previousStable` — код `previousStable` не в дельте r3→r4, подтверждён в r1/r2/r3 (см. цепочку в `CODE-REVIEW-514-r3.md`, раздел «Унаследовано из r2»).
- Документация вне `PROCESS.md:704-706` (`AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/TESTING.md`, `docs/specs/README.md`) — не в дельте, была сверена построчно в r3.
## Дешёвые гейты — уже подтверждены на этом SHA
Validate на `0ce5e4ee` зелёный: https://github.com/Matysh/houseplan-card/actions/runs/34406969841 (в тексте раунда это дано как факт; проверил `head_sha` не переспрашивал повторно — ссылка та же, что в условии раунда). Значит `npx tsc --noEmit`, `npm test` (полный), `npm run build` со сверкой бандла не перегонялись — правило сужения гейтов прямо это разрешает. Дополнительно лично прогнано (диф это код меняет):
- `node --test test/e2e-gate.test.mjs test/release-workflow.test.mjs` → 15/15.
- Мутация на M1 (откат защиты → тест краснеет) — см. выше.
- Три именованных мутанта `mutation-gate.mjs` → 1/1 каждый.
- `node scripts/process-gate.mjs --issues` → «гейт пройден, предупреждений 1» (п.8, ожидаемо — класс A не задет).
**Не прогонялось и почему:** `golden:verify` (дельта не меняет рендер), `check-docs.mjs`/`model-invariants.mjs`/браузерные смоки (дельта не трогает `src/**`, геометрию или `layout`), `pytest tests_backend` (Python не тронут), performance-профили (не названы в AC, перф-путь не тронут). Полный список `demo/smoke_*.mjs`/`smoke-select.mjs` не запрашивал — выбирать нечего, `src/**` не в диапазоне ни разу за все 4 раунда.
**Одно число — один источник.** Неприменимо: дельта не добавляет и не меняет ни одной пользователем видимой величины (изменения — в тексте ошибки CI-скрипта и в формулировке `PROCESS.md`).
## Новая находка
### L5 (Low, новая в r4) — `docs/specs/514-e2e-stable-release-gate.md:38` дословно цитирует старый текст `TOKEN_HINT`, который правка этого раунда изменила
Спецификация иллюстрирует ошибку отказа токена цитатой: «текстом «нужен секрет `E2E_DISPATCH_TOKEN` с правом Actions: write на houseplan-e2e»». После фикса M1 фактический `TOKEN_HINT` (`scripts/e2e-gate.mjs:28`) — «нужен секрет `E2E_DISPATCH_TOKEN`: Actions: write на houseplan-e2e И чтение релизов houseplan-card (fine-grained PAT — оба репозитория в списке)». Цитата в ТЗ устарела относительно кода.
Не блокирует: ТЗ — иллюстративный, не нормативный текст в этом месте (описывает пример сообщения, а не контракт с проверяемым AC на точную строку), спецификации не переоткрываются задним числом при каждой правке реализации (§2.3 — статус ТЗ не меняется после `S5-ready`), и ни один AC/тест не сверяет спек-текст с `TOKEN_HINT` дословно. Оставляю Low без правки, с записью: если задача вернётся на ещё один правочный цикл по другой причине, эту строку стоит поправить заодно.
## Что проверено и корректно
- M1(r3) закрыт по существу, а не косметически: `releases()` теперь симметричен `dispatch()` — оба бросают на `r.status !== 0` и оба ловятся одним и тем же `catch` в `e2eGate`, давая `result: 'error'` с подсказкой по токену. Подтверждено мутацией (откат → тест краснеет), не только чтением.
- `TOKEN_HINT` теперь называет оба репозитория и обе операции — точное описание того, что нужно фолбэк-токену, чтобы не наступить на M1 повторно.
- L4(r3) исправлена текстуально верно: подлежащее (`release.yml`) и дополнение (`houseplan-e2e`/`e2e.yml`) на своих местах.
- Дельта не расширяет скоуп: оба коммита правят ровно то, что назвал вердикт r3 (M1 + L4), новых файлов/модулей не добавлено, кроме публикации самого документа r3.
- Трейлеры обоих коммитов корректны (`Issue: #514`, `User-Visible: no`); `User-Visible: no` по-прежнему верно — правка видна только в тексте ошибки CI-гейта и в `PROCESS.md`, не пользователю продукта.
- База `origin/dev` не сдвигалась между r3 и r4 — условие для сокращённого делта-разбора выполнено, а не предположено.
## Чего не проверял и почему
- `tsc --noEmit`, `npm test` (полный), `npm run build` с попиксельной сверкой бандла — зелёный Validate на `0ce5e4ee` уже покрывает (ссылка на прогон дана в условии раунда).
- `golden:verify`, `check-docs.mjs`, `model-invariants.mjs`, `pytest tests_backend`, браузерные смоки — дельта не трогает `src/**`, геометрию/`layout`/толщины или Python; так было во всех четырёх раундах этой задачи.
- Реальный dispatch `release.yml` по событию `release: published` с настоящим `E2E_DISPATCH_TOKEN` — секрет ещё не заведён владельцем (это и был контекст M1 — риск в конфигурации, которая пока не существует), недоступно ревьюеру. Первая живая проверка — следующий stable-релиз, как честно называет ТЗ.
- README `houseplan-e2e` — не перечитывал повторно, вне диапазона `origin/dev...HEAD` во всех раундах, контракт имени job (единственное, от чего зависит код) был сверен напрямую в r3 и код, от которого он зависит, не менялся.
## Продуктовое рассуждение
Дельта не меняет продуктовую оценку r3: задача по-прежнему укрепляет цепочку поставки (гейт стабильного релиза дожидается настоящего E2E), в скоуп `docs/SCOPE.md` не входит напрямую (инфраструктура процесса), беты не задевает. M1 был риском в конфигурации, которую владелец ещё не завёл; фикс убирает этот риск заранее, не дожидаясь, пока он реализуется в проде. Ни один AC не ослаблен правкой — только усилен (releases() больше не может тихо соврать).
## Вопросы владельцу
Нет открытых продуктовых вопросов.
---
## Материал раунда
- Ветка: `issue/514-e2e-stable-release-gate`, коммит `0ce5e4eed3d1d2e3a52f07c35dbc9a8297dd01a2`.
- Диапазон: `origin/dev..HEAD` (9 коммитов), `origin/dev` = `ad2858a80cc7be36a8577c8b05ea330dca66eba5`.
- ТЗ: `docs/specs/514-e2e-stable-release-gate.md`; ревью ТЗ: `docs/reviews/SPEC-REVIEW-514-r1.md` (зелёное, r1).
- Предыдущий код-ревью: r3, жёлтый, материал `d166bad3eb293d70cdd37cc715c639449742d988` (осиротел после ребейза; content-эквивалент восстановлен как `a8a9b7d99e61af685149c903d34fab84c1008d90`, см. раздел выше).
- Дельта r3→r4: `git diff a8a9b7d9..HEAD` — 2 коммита (`ed6d1d76` docs-only, `0ce5e4ee` M1+L4 фикс), 3 файла кода/процесса, +24/−5.
- Validate на материале раунда: https://github.com/Matysh/houseplan-card/actions/runs/34406969841 (success, дано условием раунда).
- Вердикт этого раунда: **зелёный** · High 0 · Medium 0 · Low 4 (L1-L3 унаследованы; L4 закрыта фиксом этого раунда; L5 новая, принята без правки).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/514-e2e-stable-release-gate`, коммит `0ce5e4eed3d1` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `55fd573338326d3dbde36a2e4444439cce493833`
```
git log --all --format='%H %T' | grep 55fd57333832
```
- ТЗ `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