docs: review document for #536

Issue: #536
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-12 04:06:12 +00:00
parent 1e1880f010
commit bb031d8599
+140
View File
@@ -0,0 +1,140 @@
# CODE-REVIEW-536-r1
**Issue:** #536 «Плашка версий не гарантирует своё исчезновение: контроллер меняет состояние, не прося перерисовку»
**Заход:** r1 · блокирующих циклов израсходовано 0 из 2 (лёгкий трек)
**Материал:** ветка `issue/536-banner-notify`, HEAD `1e1880f010495fe23197feddc64e33138445fc5d`,
база `origin/dev` = `05a2c68d`. Ровно один коммит в диапазоне `origin/dev..HEAD`.
Предыдущая попытка запуска ревью на этом issue (комментарий «Ревью не запускалось») не
вынесла вердикта: конвейер сам перебазировал ветку на `dev` между хендоффом и стартом
Validate, гонка диспатча против ребейза, а не красный код. Кода никто не читал. Это
честно первый заход код-ревью, разбор — полный.
## Скоуп
Одна строка поведенческого кода в `src/version-recovery.ts` (`disconnect()`), плюс
свидетель (`test/version-recovery.test.mjs`, 2 новых теста), плюс регистрация мутанта
(`scripts/mutation-gate.mjs`), плюс пересборка трёх копий бандла и обновление отпечатка
скриншотов документации. Ровно то, что названо в ТЗ («Затронутые файлы»).
## Как проверялось
Прочитан `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md`, тело issue #536 целиком (постановка +
ТЗ), все комментарии (аналитика/ТЗ на одном ходу → зелёное ревью ТЗ r1 → хендофф →
гоночный false start → возврат на код-ревью без нового коммита). Прочитан полный текст
`src/version-recovery.ts` (не только диф) и вызывающий код в `src/houseplan-card.ts`
(`disconnectedCallback`, `requestUpdate` override, `_dangerConfirmController`,
`_clearRoomFocus`) — чтобы независимо проверить риск №1 из ТЗ («вызов `requestUpdate()` из
`disconnectedCallback` — путь не новый»), а не поверить автору на слово: `_cancelDangerConfirm()`
уже сегодня триггерит реактивный сеттер (`_dangerConfirm = state`) из того же
`disconnectedCallback`, до `super.disconnectedCallback()` — тот же порядок вызовов, тот же
путь через Lit. Прецедент подтверждён чтением, не с чужих слов.
Диф прогнан построчно вручную (см. таблицу AC ниже) — трассировка обоих новых тестов
через `_reconcile()`/`_showBanner()`/`disconnect()` шаг за шагом с константами по умолчанию
из `harness()`/`update()`, результат совпал с зафиксированными в тесте ожиданиями.
### Гейты — что прогнано и почему остальное не прогонялось
| Гейт | Статус | Источник |
|---|---|---|
| `typecheck` / `test` / `build` (+ сверка 3 копий бандла) | **не перегонял** | Validate на точном SHA `1e1880f0` завершился `success` (ссылка в постановке раунда); дешёвые гейты сошлись на этом прогоне |
| сверка `dist/` ↔ `custom_components/houseplan/frontend/` | **прогнал сам** | `cmp` трёх файлов (`houseplan-card.js`, `houseplan-panel.js`, `houseplan-assets.json`) — побайтово совпадают |
| `node scripts/check-docs.mjs` | не перегонял отдельно, но факт зафиксирован | diff трогает `src/**` → обязателен; автор прогнал `npm run docs:accept -- --identical` (11 кадров совпали попиксельно, обновлён только отпечаток исходников) — засчитано, `check-docs` в Validate тоже входит в тот же зелёный прогон |
| `node scripts/mutation-gate.mjs --id=version-banner-disconnect-silent` | **не перегонял**, но проверил логику мутанта чтением | патч мутанта откатывает ровно добавленную строку `this.hooks.changed()`; заявленный автором результат «1 из 1» правдоподобен и я independently проследил, что это красит именно новый тест «notice dropped», а не оба |
| `node demo/smoke_version_recovery.mjs` | не перегонял | заявлен зелёным автором; диф не меняет DOM/рендер, только счётчик перерисовок на уровне контроллера — смок этого не видит по конструкции (ниже) |
| `node scripts/model-invariants.mjs` | не применим | diff не трогает геометрию, `layout`, толщину, `open_spans` |
| `python -m pytest tests_backend` | не применим | Python не тронут |
| `golden:verify` | не применим | диф не меняет визуальный результат (только внутренняя логика запроса перерисовки, не сама разметка) |
| перф-профили | не применим | не названы в AC, файлы вне `src/iso-*`/`src/live-*`/`src/render-*` |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | приложен автором | вывод: «НЕОПРЕДЕЛЁННОСТЬ: дифф исполняемый, но ни один смок не связан доказуемо», символ `_banner` не встречается ни в одном смоке — согласен с decision автора не прогонять широкий набор: `_banner` приватное поле, наблюдаемый факт («попросили ли перерисовку») не проявляется в DOM браузерного смока, там его снимает любая посторонняя перерисовка — то самое совпадение, которое чинит задача |
Дешёвые гейты не перегонялись сознательно, а не пропущены молча: Validate на точном SHA
материала зелёный (см. ссылку в постановке раунда), сверка бандла — прогнана мной лично.
## AC · чем доказан · чем краснеет
| AC | Доказательство | Проверка ревьюером |
|---|---|---|
| **AC1** — последовательность «несовпадение → disconnect → версии сошлись → connect» просит ровно 2 перерисовки, плашка пуста в конце | тест `#536 a notice dropped on disconnect asks the host to repaint` | Прослежен построчно: `update({reducedMotion:false})` до `connect()` не триггерит `changed()` (не подключён → `_hideBanner()` на пустом поле — no-op); `connect()` → mismatch → `_showBanner` → `changed()` #1, `banner.phase==='visible'`; `disconnect()` → banner не пуст → `changed()` #2, banner=null; `update({backendVersion:'1.72.0'})` → relation `equal`, не подключён → no-op; `connect()` → relation `equal` (не mismatch) → `_hideBanner()` на пустом поле — no-op. Итог: 2 события `changed`, banner `null`. Совпадает с утверждениями теста. **Мутация доказана логически**: без `this.hooks.changed()` в `disconnect()` второе событие не производится, `repaints()` после `disconnect()` останется `1` — ассерт `assert.equal(repaints(), 2, …)` красный |
| **AC2** — `disconnect()` без плашки перерисовку не просит | тест `#536 a disconnect without a notice asks for nothing` | Прослежен: `update({backendVersion:'1.72.0'})` (frontend по умолчанию `1.72.0`) → relation `equal`; `connect()` → не mismatch → `_hideBanner()` на пустом поле — banner остаётся `null`, `changed()` не звался; `disconnect()` → `if (this._banner)` ложно → тело условия не выполняется, `changed()` не звался. `before === after`. **Обратная мутация** (безусловный `changed()` в `disconnect()`) красит именно этот тест — подтверждено чтением условия `if (this._banner) { …; this.hooks.changed(); }`: без охраны событие добавилось бы в любом случае |
| **AC3** — прежние утверждения контроллера не сдвинулись (таймеры, токены, kiosk-путь, reduced motion) | 11 существующих тестов `test/version-recovery.test.mjs`, ни один не тронут | Диф `test/version-recovery.test.mjs` — только добавление (`+39`, `0` изменений в существующих строках), подтверждено `git show` построчно. Логика `disconnect()` вне добавленного `if`-блока идентична коду `origin/dev` (сверено `git show origin/dev:src/version-recovery.ts` против диффа) |
Все три AC доказаны либо тестом с прослеженной логикой мутации, либо построчным чтением
диффа против базы. Third-column (`чем краснеет`) для AC1/AC2 — не голое «tест умеет
падать», а конкретно прослеженная причинно-следственная связь между снятой строкой и
конкретным упавшим ассертом; для AC3 — «проверено чтением, не исполнением» в буквальном
смысле §2.7.
## Проверено и корректно
- Правка ограничена ровно объявленной строкой (`disconnect()` в
`src/version-recovery.ts:213-216`); `_hideBanner()`, `_reconcile()`, kiosk-таймер,
`claimReloadTarget`, токены анимации — не тронуты (сверено построчно с `origin/dev`).
- Комментарий рядом с правкой объясняет причину («Lit renders neither on disconnect nor on
reconnect») — не голословно: это тот же факт, что и в докстринге модуля и в постановке
issue (замер со счётчиком на собранном модуле).
- Прецедент «`requestUpdate()`/реактивный сеттер из `disconnectedCallback` не ломается» —
независимо подтверждён чтением `_cancelDangerConfirm()` → `HpConfirmController.cancel()`
→ `resolve()` → `this._changed(null)` → `this._dangerConfirm = state`, вызываемого из
`disconnectedCallback` (`houseplan-card.ts:2743-2751`) **до** `super.disconnectedCallback()`
— тот же порядок, тот же путь, работает уже сегодня.
- Три копии бандла (`dist/`, `custom_components/houseplan/frontend/`) побайтово совпадают
(проверено `cmp` лично, не с чужих слов).
- Кажущийся большой диф сгенерированных чанков (`dist/houseplan-assets/*`, переименования
хешей, добавление/удаление `guard-*.js`) — проверен построчно на образцах
(`iso-scene-render`, `guard-*`): единственные реальные отличия — встроенная константа
`__HOUSEPLAN_BUILD_FINGERPRINT__` (считается по всему `src/**`, поэтому меняется от
любой правки фронтенда) и путь импорта на переименованный главный чанк
(`houseplan-card-B09T8YNT.js` → `houseplan-card-kxu4PK0e.js`, чьё содержимое реально
изменилось из-за правки `version-recovery.ts`, находящегося в eager-графе). Каскад
переименований механический, не посторонняя правка.
- `User-Visible: no` и лёгкий трек — решения, принятые на зелёном ревью ТЗ r1
(`docs/reviews/SPEC-REVIEW-536-r1.md`), код им не противоречит: ни одно уже
наблюдавшееся поведение не изменилось (это и есть предмет AC3), changelog не тронут —
корректно для этого случая.
- Трейлеры коммита корректны: `Issue: #536`, `User-Visible: no`, ровно один коммит.
- «Одно число — один источник»: неприменимо, диф не добавляет и не меняет ни одной
величины, видимой пользователю.
## Находки
Нет High. Нет Medium. Нет Low сверх уже снятых на ревью ТЗ (термин «контроллер» в
формулировке «До/После» и невыписанные явно строки DoR — оба сняты решением ревьюера ТЗ
без возврата, см. `SPEC-REVIEW-536-r1.md`; повторно не поднимаю, так как это тот же
раунд документации, не код).
## Чего не проверял
- `typecheck`/`test`/`build` лично не перегонял — доверился зелёному Validate на точном
SHA материала (ссылка в постановке раунда), сверил только производный артефакт (бандл).
- `mutation-gate.mjs` и `demo/smoke_version_recovery.mjs` не перегонял руками — принял
заявленные автором результаты, но независимо проверил логику первого чтением кода
мутации (см. таблицу AC) вместо слепого доверия.
- Не проверял поведение в реальном браузере (ручного тестирования в цикле нет по
процессу); визуальный сценарий не пострадал бы в любом случае, так как диф не меняет
разметку/стили.
## Материал раунда
- SHA: `1e1880f010495fe23197feddc64e33138445fc5d`
- Дерево: единственный коммит в диапазоне `origin/dev..HEAD`
- База: `origin/dev` = `05a2c68d`
---
**Вердикт: зелёный · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 0 → в задаче**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/536-banner-notify`, коммит `1e1880f01049` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `8d2cba9fc9bd684598fa246f90c09485b914e070`
```
git log --all --format='%H %T' | grep 8d2cba9fc9bd
```
- Тело issue: `fdc4c4f20bae1574a65b8c8bac06c43f4ecdab6d3394149baea09c34bec75cc4`
- Вердикт конвейера: `green` · High 0