docs: review document for #756

Issue: #756
User-Visible: no
This commit is contained in:
claude[bot]
2026-10-01 12:17:24 +00:00
parent 7b7d6582e6
commit 708d5cf5cc
+200
View File
@@ -0,0 +1,200 @@
# CODE-REVIEW-756-r1
Материал раунда: ветка `issue/756-warm-dialog-pending-mode` @ `7b7d6582e69cbf3f4b2b5398b063ad22056e5d21`
поверх `dev` `7ce3f662`. Диапазон: `git log --oneline origin/dev..HEAD` — один коммит;
`git diff origin/dev...HEAD` — 9 файлов, +230/−47. Трек: `track:show`, заход r1,
блокирующих циклов 0/2.
## Скоуп
Баг тёплого ре-маунта: когда право записи (`hass`/`can_write`) приходит уже
после вставки карточки в DOM, режим редактора откладывается в
`_pendingNavMode` и позже входит через `_resumePendingNavMode → _setMode`.
Этот путь не вызывал `_warmReviveDialog`, поэтому открытый диалог с
черновиком молча терялся, а флаг `_warmRevivePending` оставался поднятым —
следующий ре-маунт в цепочке A→B→C воскрешал чужой черновик предшественника.
Правка выносит общий хвост тёплой адопции (воскрешение черновика ровно один
раз + удержание refit до оседания редакторской панели) в новый модуль
`src/warm-mode-adoption.ts` (`finishWarmModeAdoption`, `resumeWarmMode`) и
применяет его на обоих путях: немедленном (`_requestMode(..., adopt=true)`) и
отложенном (`_requestMode(..., adopt='resume')` / прямой вызов
`resumeWarmMode` из `_resumePendingNavMode`, когда рантайм уже есть).
`docs/WARM-REMOUNT.md` §2 дополнен абзацем, описывающим именно этот механизм.
Работа закрывает J6 (SCOPE.md: «keep the plan true», три редактора, drag/
resize — сюда же относится устойчивость состояния редактора к техническому
пересозданию карточки).
Один коммит, трейлеры на месте: `Issue: #756`, `User-Visible: yes`; оба
CHANGELOG правлены в этом же коммите (RU и EN, формулировка совпадает с ТЗ
issue дословно).
## Риск по изменённым участкам (#707)
- **perf** — `src/houseplan-card.ts:761` (удалённый) и
`src/warm-mode-adoption.ts:35` (новый) токен `requestAnimationFrame`.
Это не новая перф-поверхность: до правки существовал ровно один такой
двойной-rAF (в немедленной ветке адопции, `_requestMode(..., adopt=true)`),
и после правки он остался ровно тем же механизмом, перенесённым в
`finishWarmModeAdoption` без изменения логики (построчно сверено —
см. «Что проверено чтением» ниже) — старый токен не исчезает, а
перемещается. Единственное фактическое расширение: та же процедура теперь
используется ещё из `resumeWarmMode` (отложенный путь) — но это и есть
заявленное поведение задачи, зафиксированное в
`docs/WARM-REMOUNT.md` §2 (правка этого же диффа, абзац «Отложенный
редактор — та же тёплая адопция, только позже...») и доказанное AC1
(`watchView` проверяет вьюпорт покадрово после ре-маунта с поздним
`hass`/`can_write` — именно это окно и использует новый rAF). Критерий
§5 `perf-touch` пройден предметно, а не отпиской: поведение документировано
и покрыто тестом, который я исполнил сам (см. гейты). `route: fix`,
reclassify не требуется.
Прочие классы из §5 не нарушены: одна поверхность (`src/houseplan-card.ts` +
новый выделенный модуль того же механизма — ровно то, что назвал владелец в
оценке issue), миграции/compat-полей нет, нового UX-контракта нет (видимое
поведение совпадает с уже описанным в WARM-REMOUNT.md), touch не затронут.
Трек `show` подтверждён владельцем явно в оценке issue («трек: show
(... перфа, touch и миграции нет)»).
## Как проверялось
Дешёвые гейты уже зелёные на этом SHA (Validate,
https://github.com/Matysh/houseplan-card/actions/runs/36858886158) — не
перегонял отдельно, но они шли и локально как побочный эффект подготовки
окружения для браузерных смоков (см. ниже) и тоже зелёные.
| Гейт | Статус | Примечание |
|---|---|---|
| `npx tsc --noEmit` | ✅ | без вывода |
| `npm test` | ✅ | 3465 passed / 1 skipped / 0 failed |
| `npm run build` + `bundle-sync` | ✅ | собрано, скопировано в `demo/srv/assets`; рабочее дерево возвращено `npm run bundle:clean` |
| `node scripts/mutation-gate.mjs --check` | ✅ | `warm-pending-mode-leaves-revive-waiting` применяется без конфликта (`ok`); 204/200 browser guards — рост объяснён строкой в `docs/testing-notes/mutation-browser-guards.md` (сам диф) |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | ✅ выполнен | см. раздел ниже — решение по каждой строке |
| `demo/smoke_warm_dialogs.mjs` (все разделы, включая H) | ✅ зелёный на HEAD | **и воспроизведено красным на `origin/dev`**: применил новый файл смока к коммиту `7ce3f662` (worktree), собрал — ровно 14 несовпадений, список совпадает с заявленным автором 1:1 (`hAfterInsert*`, `hNextTask*`, `hNonAdminCanWrite*`, `hChainCarriesOwnDraft`, `hSpaceSwitchReviveSettled`) |
| Мутант `warm-pending-mode-leaves-revive-waiting` | ✅ проверен исполнением | вручную применил патч из реестра к `src/warm-mode-adoption.ts`, пересобрал, прогнал `smoke_warm_dialogs.mjs` — те же 14 падений, что и на dev. Тест умеет падать, мутант действительно убивается названным смоком |
| `demo/smoke_warm_remount.mjs` | ✅ | AC3 |
| `demo/smoke_warm_owners.mjs` | ✅ | AC3 |
| `demo/smoke_nav_persist.mjs` (включая `pendingModeUsesTransitionAuthority`) | ✅ | AC3 — отдельно проверил ключ в выводе |
| `demo/smoke_config_reload_race.mjs` | ✅ | AC3 |
| `node --test test/core-file-budget.test.mjs` | ✅ | потолок 12896 не поднят; `wc -l src/houseplan-card.ts` = 12891 (у автора в комментарии 12892 — расхождение на одну строку, не влияет на вердикт потолка, не являюсь уверенным, что это не разница в подсчёте конечного перевода строки) |
| `node --test test/smoke-select.test.mjs` | ✅ | валидирует, что новая запись `smoke-links.mjs` указывает на существующие смоки/символы |
## Решение по строкам `smoke-select`
Матрица: 286 смоков, порог «широкого» символа — 57. Вывод: 35 прямых
совпадений, 54 слабых, 1 зарегистрированная связь, `_setMode`/`requestUpdate`
исключены как слишком широкие.
- **Прогнал явно** (4 из 35 прямых + 1 зарегистрированная): `smoke_warm_dialogs`
(прямое, названо в AC1/AC2), `smoke_nav_persist` (прямое, названо в AC3),
`smoke_warm_owners` (прямое, названо в AC3), `smoke_warm_remount`
(зарегистрированная связь — она явно указана как наблюдатель новых
экспортов `finishWarmModeAdoption`/`resumeWarmMode`/`_holdWarmRefit`/
`_releaseWarmRefit`), плюс `smoke_config_reload_race` (назван в AC3,
хотя в выборке отсутствует — изменённые строки не задевают его символы
напрямую, но ТЗ требует его явно).
- **Не прогнал остальные 31 прямое и 54 слабых — решение разобрать чтением**:
все они матчатся по `_mode`, `_view`, `_editorRuntime`, `_ensureEditorRuntime`,
`_requestMode`, `_stageEl`, `_pendingRefitSize`, `_refitRaf`,
`_lastValidStageSize`, `_warmRevivePending` — символам, которые
`_requestMode` читает/пишет и до, и после рефакторинга. Построчно сверил
обе ветки (`adopt === true` и `adopt === false`) со старой версией функции:
поведение не изменилось — то же вычисление `request`, то же решение
«можно ли войти» (`ready`/`current` эквивалентны старым двум последовательным
проверкам), тот же `_adoptMode`/`_setMode`, тот же двойной rAF, перенесённый
в `_releaseWarmRefit`/`finishWarmModeAdoption` без изменения порядка
присваиваний, влияющего на результат. Единственная новая ветка — `adopt ===
'resume'`, и единственные вызовы с этим значением — `src/houseplan-card.ts:4167`;
второй новый путь (прямой вызов `resumeWarmMode`, когда рантайм уже есть) —
`src/houseplan-card.ts:4169` (новая строка в `_resumePendingNavMode`). Ни
один из 85 оставшихся смоков `_requestMode` с `adopt: 'resume'` не вызывает
и `_resumePendingNavMode`/`resumeWarmMode`/`finishWarmModeAdoption`/
`_holdWarmRefit`/`_releaseWarmRefit` не называет — их совпадение с диффом
целиком объясняется тем, что они используют тот же режим/вьюпорт
инфраструктуры в не затронутых ветках. Это «проверено чтением, не
исполнением», а не пропуск.
## Находки
Нет. High: 0, Medium: 0, Low: 0.
## Что проверено чтением (построчно, без исполнения)
- `_requestMode` (houseplan-card.ts:734–761): ветки `adopt===true` и
`adopt===false` после рефакторинга байт-в-байт эквивалентны версии до
правки (см. разбор выше) — старое поведение не меняется ни для одного
существующего вызова.
- `_warmReviveDialog(settle)` (houseplan-card.ts:3468 и далее): параметр
`settle=true` НЕ означает «съесть черновик безусловно» — он лишь отключает
повторное взведение `_warmRevivePending` («подождать ещё»); дальше функция
идёт по тем же условиям §3 (owner жив/TTL, совпадение `space`/`mode`) и либо
открывает диалог, либо роняет его. Это соответствует тексту
WARM-REMOUNT.md §2: «камера, уже ушедшая сама... остаётся обычному рефиту,
а черновик проверяется по §3 п.4» — черновик не отбрасывается автоматически
только из-за того, что камера разошлась с памяткой, он по-прежнему
открывается, если совпали пространство и режим. Подтверждено и исполнением:
раздел H покрывает как совпадающий (hAfterInsert/hNextTask/
hNonAdminCanWrite — камера совпала, диалог открылся), так и несовпадающий
случай (hSpaceSwitch — пространство разошлось, диалог съеден).
- `resumeWarmMode` (warm-mode-adoption.ts:54–64): `kept` сравнивается с
текущим `_view` ДО вызова `commit()` — то есть с видом, который памятка
ещё не трогала, а не с видом после рефита `_setMode`. Это соответствует
замыслу «камера, которую окно ожидания не трогало, возвращается
бит-в-бит» — сравнение именно с состоянием непосредственно перед входом в
редактор, а не после.
- Единственный вызов с `adopt==='resume'` (`:4167`) срабатывает только когда
`!this._editorRuntime` (рантайм ещё не загружен — ожидание в `_requestMode`
само дождётся `_ensureEditorRuntime()`), иначе (`:4169`) вызывается
`resumeWarmMode` напрямую с `_setMode` синхронным коммитом — ветвление
корректно покрывает оба случая «рантайм ещё грузится»/«рантайм уже есть»,
которые в противном случае гонялись бы по разным путям без реального
различия в итоговом поведении.
## Что проверено исполнением
Раздел «Как проверялось» выше: typecheck, unit-тесты, сборка, все названные в
AC3 браузерные смоки плюс полный `smoke_warm_dialogs.mjs` (включая новый
раздел H), мутант применён и исполнен вручную, `smoke-select` запущен и его
строки разобраны. Красный результат на `dev` воспроизведён самостоятельно
(не только заявлен автором) — 14 из 14 заявленных проверок совпадают.
## Чего не проверял
- Полная матрица `smoke-select` (286 смоков) — не гейт ревью, предрелизная
обязанность; 85 оставшихся (прямых + слабых) совпадений разобраны чтением,
не исполнением (см. выше).
- `golden:verify` — не запрашивался (нет метки `ci:golden`, диф не меняет
геометрию/рендер плана, только тайминг входа в редактор и воскрешение
диалога).
- `python -m pytest tests_backend` — диф не трогает `custom_components/**/*.py`.
- `npm run invariants` — диф не трогает геометрию модели или ссылки на неё.
- performance-профили — не названы в AC.
- Поведение при реальной Home Assistant (не демо-песочнице) и при настоящей
задержке ответа сервера `can_write` — проверено только синтетическим
`callWS`-перехватом смока (as designed by the author); это соответствует
объёму track:show.
- Конфликт с #757 (CHANGELOG, место в реестре мутантов, счётчики инвентаря
гвардов), упомянутый автором в комментарии к задаче, — это вопрос
последующего ребейза при слиянии, а не дефект текущего SHA; материал этого
раунда не содержит #757.
## Вердикт
Зелёный. route: fix (критерии §5 пройдены предметно, не отпиской — см.
«Риск по изменённым участкам»). AC1–AC3 доказаны исполнением тестов, которые
я сам прогнал и которые умеют падать (воспроизвёл красный `dev` и убитый
мутант). Находок нет.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/756-warm-dialog-pending-mode`, коммит `7b7d6582e69c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `1f6e1300ddf8b902ddb741a6291e9cbf29e4b5db`
```
git log --all --format='%H %T' | grep 1f6e1300ddf8
```
- Тело issue: `ba4c5568a2f2b4280acbd5dda51e7bbfe8e2d797f4de12ef083b16502198e57a`
- Вердикт конвейера: `green` · High 0 · маршрут `fix`