From 708d5cf5cc39c061582613879336ca00cc7befa5 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:17:24 +0000 Subject: [PATCH] docs: review document for #756 Issue: #756 User-Visible: no --- docs/reviews/CODE-REVIEW-756-r1.md | 200 +++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/reviews/CODE-REVIEW-756-r1.md diff --git a/docs/reviews/CODE-REVIEW-756-r1.md b/docs/reviews/CODE-REVIEW-756-r1.md new file mode 100644 index 00000000..0f1589f7 --- /dev/null +++ b/docs/reviews/CODE-REVIEW-756-r1.md @@ -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` и убитый +мутант). Находок нет. + +--- + + + +## Материал раунда + +- Ветка: `issue/756-warm-dialog-pending-mode`, коммит `7b7d6582e69c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `1f6e1300ddf8b902ddb741a6291e9cbf29e4b5db` + ``` + git log --all --format='%H %T' | grep 1f6e1300ddf8 + ``` +- Тело issue: `ba4c5568a2f2b4280acbd5dda51e7bbfe8e2d797f4de12ef083b16502198e57a` +- Вердикт конвейера: `green` · High 0 · маршрут `fix`