Files
houseplan-card/docs/reviews/CODE-REVIEW-536-r1.md
T
2026-09-12 04:06:12 +00:00

16 KiB
Raw Blame History

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 → в задаче


Материал раунда

  • Ветка: issue/536-banner-notify, коммит 1e1880f01049 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 8d2cba9fc9bd684598fa246f90c09485b914e070
    git log --all --format='%H %T' | grep 8d2cba9fc9bd
    
  • Тело issue: fdc4c4f20bae1574a65b8c8bac06c43f4ecdab6d3394149baea09c34bec75cc4
  • Вердикт конвейера: green · High 0