Files
houseplan-card/docs/reviews/CODE-REVIEW-520-r2.md
T
2026-09-10 17:47:45 +00:00

31 KiB
Raw Blame History

CODE-REVIEW-520-r2

Issue: #520 · Этап: code · Заход: r2 · Материал: 462b56453cd6948c083e62ad8c92ec29aeb022b2 Вердикт: жёлтый · High: 0 · Medium: 1 (в скоупе)

Скоуп

Повторный заход код-ревью после красного r1 (docs/reviews/CODE-REVIEW-520-r1.md, находка High: снятие _serverCfg/_layout из static properties не устраняло лишнюю эпоху конфигурации на старте — эпох осталось 4 из 4, бюджеты large-house-interaction красные). Дельта r1→r2 — один коммит правки 462b5645 поверх материала r1 (a6258715) плюс публикация документа r1 (49a02637, публикационный коммит без кода).

Дельта НЕ локальна в смысле §2.9: автор сам характеризует её как «закрыт неверный диагноз» — между r1 и r2 сменился сам механизм фикса (не косметика формулировки, а новый контракт: атомарность усыновления через afterAdopt). Дельта затрагивает новую поверхность (src/config-adoption.ts — хук GatedAdoptionInput.afterAdopt, порядок вызовов внутри adoptAuthoritativeGated; src/houseplan-card.ts — перенос блока _loadFromServer/_reloadConfigOnly). Это ровно тот случай из инструкции: «смена контракта поведения» — разбор полный, а не только по одной находке r1, но с наследованием того, что дельта не задевает (AC1/AC2 как код, гигиена ветки, бюджет бандла №367, release- артефакты).

Диапазон материала: git log --oneline origin/dev..HEAD — 10 коммитов (37ef970d…462b5645). Диф r1→r2 (git diff a6258715..462b5645) продуктового кода: src/config-adoption.ts +18/−4, src/houseplan-card.ts +38/−28 (без учёта переноса блока построчно), scripts/bundle-budget.mjs +8 (потолок 300 300 → 300 400), scripts/mutation-gate.mjs +25 (два новых мутанта), test/config-adoption.test.mjs +43, test/config-adoption-ownership.test.mjs +40. Остальное в git diff --stat origin/dev...HEAD — бандл (три копии, синхронно), доки предыдущих раундов, .gitignore.

Как проверялось

Тело issue #520 изменилось после зелёного ревью ТЗ r3 (записанный хеш 60492a3aec5f…); текущий хеш тела — 4524f04412... (сверено sha256sum по gh issue view --json body), отличается. Это находка по #517, называю её здесь: ТЗ переписано пост-фактум (раздел «Причина» — с диагноза wrapped на измеренный await, «Контракт» получил п.1 про атомарность и AC6). Автор сам заявил об этом в хендоффе («если ревьюер сочтёт, что это требует возврата в S4-spec-review, возражать не буду»). AC сверены с ТЕКУЩИМ полным текстом (раздел ниже), а не с r3-версией. Решение: возврата в spec не требую — текст описывает механизм, который я независимо перепроверил кодом и измерением (см. ниже), новых продуктовых развилок или пользовательского поведения текст не вводит, User-Visible: no не меняется. Это дисциплинированное обновление ТЗ вслед за находкой r1, а не смена скоупа.

  1. Прочитан весь диф git diff a6258715..462b5645 (r1→r2) и общий git diff origin/dev...HEAD целиком (не только имена файлов).
  2. Прочитан src/config-adoption.ts: новый хук afterAdopt на GatedAdoptionInput, место вызова — синхронно последним перед return, без await/.then( между усыновлением и хуком (строки 401-412, 428-459). Единственный предшествующий await (host._signer.prepareImage) стоит ДО adoptStructuralResponses, т.е. до замены тела — атомарность блока «замена тела → хвост → afterAdopt» им не нарушается.
  3. Прочитан src/houseplan-card.ts: _loadFromServer — восстановление вьюпорта, _loadOk = true и rebuildDevices() перенесены в afterAdopt; хвост в finally пересобирает устройства только если !devicesRebuilt (переменная замыкания, ровно один рестарт на попытку). _reloadConfigOnly — та же схема, короче.
  4. Прочитаны оба новых теста:
    • test/config-adoption.test.mjs — «микрозадачный зонд»: в beforeAdopt ставится Promise.resolve().then(...), и тест требует, чтобы afterAdopt оказался в трассе ПЕРЕД этим 'microtask'. Это не source-scan, а поведенческая проверка ровно того контракта, из-за которого была регрессия (перенос на микрозадачу после await — это и есть баг из r1). Прогнан локально: 6/6 pass (см. «Гейты»).
    • test/config-adoption-ownership.test.mjs — источник-скан: _loadOk = true предшествует rebuildDevices() внутри afterAdopt; _restoreZoom() внутри того же блока; после await в теле функции нет незащищённого this._maybeRebuildDevices() (это и есть сама регрессия r1, если бы она вернулась); _reloadConfigOnly идёт тем же путём. Плюс тест на adoptAuthoritativeGated: afterAdopt вызывается после хвоста и до return, без await/.then( между.
  5. Прочитаны оба новых мутанта (scripts/mutation-gate.mjs): adoption-tail-defers-caller-hook (откладывает afterAdopt на микрозадачу) и authoritative-load-seeds-devices-after-the-await (убирает rebuildDevices() из хука). Прогнаны локально по одному (node scripts/mutation-gate.mjs --id=...) — оба «поймано 1 из 1», не принято на слово из PR-комментария.
  6. Находка r1 (High) была именно про AC3–AC5 — измеренный факт, а не код. Проверил её закрытие измерением, не чтением. В песочнице нашёлся Chromium (/usr/bin/chromium), как и в r1. Поднял git worktree add /tmp/hpc-baseline-r2 a44fbd37, собрал (npm ci, npm run build, bundle-sync.mjs) оба дерева — базу и материал 462b5645 (в основной рабочей копии). Прогнал npm run benchmark:large-house-interaction -- --target-root=<base|.> --samples=7 --warmups=1 --output=... на обоих, затем node demo/performance/compare.mjs --baseline=... --candidate=... --budgets=demo/performance/budgets-large-house-interaction.json. Дважды — на профиле interaction (7 образцов) и на isometric (7 образцов, минимум для compare.mjs), полностью независимо от @sparticuz/chromium-измерений автора.
  7. npm run build и синхронизация трёх копий бандла — прогнаны в рамках п.6 (нужны были для бенчмарка); git status после — чист, три копии совпадают побайтово с закоммиченными.
  8. npx tsc --noEmit, npm test, npm run build — не перегонял третьей парой глаз: Validate на 462b5645 зелёный (https://github.com/Matysh/houseplan-card/actions/runs/34507228959), принято как в шапке задания. Но перегонял отдельные файлы (см. п.4-5 выше) и node scripts/process-gate.mjs (см. «Гейты»).
  9. node scripts/check-docs.mjs — прогнан, потому что диф трогает src/** (обязательно по инструкции ревью, не по желанию). Красный — см. «Находка Medium-1».
  10. node scripts/smoke-select.mjs --base=a6258715 --head=462b5645 — 17 «прямых совпадений», 44 «слабых связи» (символ _maybeRebuildDevices признан широким автоматически не был — порог «шире 47 смоков» не достигнут, поэтому список длинный, но легитимный). Прогнал все 17 прямых совпадений сам (полный список и результат — «Гейты»), плюс три смока, которые сам автор назвал вручную сверх выборки инструмента (smoke_new_device, smoke_warm_remount, smoke_post_write_adoption) — как выборочную перепроверку заявления «все OK», а не переверку всех двенадцати.
  11. Бюджет бандла: прочитан диф scripts/bundle-budget.mjs (потолок 300 300 → 300 400, обоснование в комментарии совпадает с ТЗ) и прогнан test/bundle-assets.test.mjs целиком — 26/26 pass, включая тесты «до потолка меньше 500 Б — это шум» и «CLI применяет потолок, а не только объявляет».
  12. Трейлеры коммитов диапазона r1→r2 (e181b08f уже был в r1; a6258715, 462b5645) — Issue: #520 и User-Visible: no присутствуют. User-Visible: no корректно: диф не меняет ничего, что видит пользователь дома (та же логика, только без лишнего прохода рендера); changelog не тронут — соответствует.
  13. node scripts/process-gate.mjs на диапазоне — «гейт пройден, предупреждений 2» (те же WARN про node_modules вне классов A/B/C/D на коммитах 37ef970d/e181b08f, что и в r1 — уже принято там как исправленная и не влияющая на git diff --stat гигиена ветки).

Находки

Medium-1 (в скоупе) — node scripts/check-docs.mjs красный на материале ревью: отпечаток скриншотов документации устарел

Диф трогает src/houseplan-card.ts и src/config-adoption.ts — это инвалидирует «визуальный» отпечаток документации (scripts/source-fingerprint.mjs хэширует буквально весь src/** побайтово, без исключений по «относится ли к рендеру»). Проверено прямым прогоном:

$ node scripts/check-docs.mjs
ERROR screenshot source fingerprint is stale; run npm run docs:capture and accept before the beta candidate (#479)

Проверено, что это именно следствие ЭТОЙ ветки, а не унаследованный долг: git checkout origin/dev -- . (текущий origin/dev = 914e8402, тот же SHA, на который опирается диапазон ревью) → node scripts/check-docs.mjs → Documentation checks passed (7 files, 12 external links). Значит на dev до ветки #520 докс-гейт зелёный, и именно коммиты этого диапазона (диагностика + сама правка _loadFromServer/config-adoption.ts) его красят. Откатил рабочую копию обратно на материал (git reset --hard 462b5645) сразу после проверки.

Почему это не мелочь, а находка по процессу этого же ревью: инструкция прямо называет цену пропуска — «Пропуск этого шага в #230 и #234 оставил dev с красным job docs до следующей задачи (#237)». В этом репозитории это устоявшаяся практика — каждая ветка, трогающая src/**, обычно везёт свой коммит «docs: refresh screenshot fingerprint …» (в истории: 172d9d5a, 8721d7d9, a56df6eb, 7b826c19 и др., включая 1950df60, сделанный именно перед этим бета-кандидатом). Ветка #520 — первая после 1950df60, тронувшая src/**, и своего коммита-обновления не привезла; в разделе «Риски» ТЗ этот пункт не упомянут вовсе (в отличие от честно расписанного риска бюджета бандла +40 Б).

Проверено, что чинится тривиально и без побочных эффектов: прогнал сам node demo/docs/capture.mjs (пересборка + пересъёмка через уже установленный Chromium) — отработал чисто, docs/images/screenshots.json и десять PNG обновились локально (ожидаемо: User-Visible: no, контент экранов не меняется, обновляется только отпечаток и байты PNG-кодека). Изменения не закоммичены и отменены (git checkout -- docs/images/) — я не правлю продукт, только диагностирую. Автору нужно добавить свой коммит npm run docs:capture && npm run docs:accept (или эквивалент) в эту ветку до мержа — иначе docs job на dev покраснеет тем же паттерном, что в #230/#234.

Серьёзность Medium, в скоупе задачи (диф этой же ветки — причина стали). Без High-находки это жёлтый вердикт с возвратом автору, отдельный issue не заводится (§202).

Закрытие раунда r1

Находка r1 Чем закрыта Где это видно
High — снятие _serverCfg/_layout из static properties не устраняло лишнюю эпоху; 4 эпохи/4 сборки, бюджеты large-house-interaction красные (modelReadyMs, firstStableRenderMs, longTask.maxSingleMs, cache.entries.cleanFloor) Автор нашёл настоящий механизм (не wrapped, а await в adoptAuthoritativeGated — вызывающий возвращается на микрозадачу позже, чем Lit успевает нарисовать усыновлённый конфиг) и добавил afterAdopt — синхронное завершение хука усыновления в той же задаче, куда перенесены восстановление вьюпорта, _loadOk и пересборка устройств Перепроверено мной независимо, не на слово: bootDiag на обоих деревьях (база a44fbd37, кандидат 462b5645) — 18 циклов / 3 сборки / 3 эпохи на обоих профилях (interaction и isometric), трасса эпох идентична базовой (_saveConfig<_syncNewDevices, _saveConfig<_seedHiddenDevices, willUpdate — без лишней первой). benchmark:compare по официальным бюджетам обоих файлов — все строки зелёные, включая cache.entries.cleanFloor = 100 (interaction) и 60 (isometric, потолок там не поднимался). Плюс микрозадачный тест-зонд и два новых мутанта («поймано 1 из 1» каждый, прогнано мной) закрепляют механизм в CI, не только в разовом измерении

Находка r1 закрыта полностью: AC3/AC4/AC5 — не просто зелёные локально (как единственное подтверждение в PR-комментарии автора), а воспроизведены мной с нуля на изолированном git worktree, тем же инструментом, что и в r1, с теми же 7 образцами на профиль. Расхождений с цифрами автора нет (861→983, 2567→2760 — другая машина, но относительно базы и лимита оба прогона внутри бюджета с запасом).

Что проверено и корректно

  • AC1 — тела _serverCfg/_layout не объявлены в static properties; комментарий-ловушка переписан честно (снята ложная причинно-следственная связь «убрать объявление чинит эпоху»); тест + мутант adoption-bodies-declared-reactive — поймано 1/1 (перепрогнан).
  • AC2 — onBodyReplaced будит обновление; мутант adoption-notifies-no-host-on-config-replacement — поймано 1/1 (перепрогнан).
  • AC3 — 18/3/3 на обоих деревьях, оба профиля — измерено мной независимо (см. «Закрытие раунда r1»).
  • AC4 — firstStableRenderMs 2759.8 ≤ 3000 (interaction, абсолютный потолок) и все 4 относительных предела (оба профиля × modelReadyMs и firstStableRenderMs) — внутри лимита; benchmark:compare на обоих профилях — 0 красных строк, измерено мной.
  • AC5 — cache.entries.cleanFloor = 100 (interaction) ≤ потолок 100; 60 (isometric) ≤ 100, потолок там не менялся — измерено мной.
  • AC6 — afterAdopt вызывается синхронно последним, до return, без await/.then( между усыновлением и хуком; порядок внутри хука (_loadOk до rebuildDevices()) — подтверждено чтением и поведенческим тестом-зондом (микрозадача), не только источник-сканом; два мутанта — поймано 1/1 каждый (перепрогнано мной).
  • Единственный вызывающий путь adoptAuthoritativeGated — _loadFromServer и _reloadConfigOnly, оба используют afterAdopt; профиль post-write в проде сейчас не используется (не тронут этой веткой).
  • Бюджет бандла: потолок 300 300 → 300 400, обоснование +40 Б совпадает с диффом хука/замыкания; test/bundle-assets.test.mjs — 26/26 pass.
  • 17 из 17 «прямых совпадений» smoke-select (сам прогнал: ws_resilience, cold_view_vacuum, optimize_coordinate_canonicalization, version_recovery, danger_confirm_branches, device_position_history, fixed_floor, glow_blending, houseplan_panel, isometric_live_touch, near_axis_optimize, optimize_coincident_partition, optimize_micro_interval, readonly_cold_start* , summary_panel, summary_warm_attach, zoom_out) — все OK. (* readonly_cold_start не запускал отдельно вторым прогоном — совпадает с уже прогнанным списком автора, принято.) Плюс три смока, названные автором сверх выборки инструмента (new_device, warm_remount, post_write_adoption) — перепрогнаны мной точечно, тоже OK.
  • npm run build + синхронизация трёх копий бандла — прогнано (нужно было для бенчмарка), git status после чист — совпадает с закоммиченным деревом.
  • node scripts/process-gate.mjs — гейт пройден, 2 предупреждения (те же, что в r1, по гигиене ветки, уже закрыты там).
  • Трейлеры — Issue: #520, User-Visible: no на обоих коммитах диапазона r1→r2.
  • ТЗ: тело issue менялось после зелёного r3 (хеш другой) — находка по #517, разобрана выше в «Как проверялось»; новых противоречий с реализацией не внесено, AC6 в тексте соответствует коду и тестам.

Унаследовано из r1 (без повторной проверки)

  • Продуктовая рамка (администратор дома, первый показ большого дома, 60 комнат/200 устройств) — совпадает с docs/SCOPE.md, r1 сверил, дельта её не касается.
  • Причина регрессии измерена, не догадка — методологически (полное сравнение циклов вместо чтения исходника Lit) уже установлено r1 как верный путь; сам диагноз r1 (wrapped) оказался неверным и заменён — это не «наследование», а предмет этого раунда (см. «Закрытие раунда r1»).
  • Гигиена ветки (симлинк node_modules, исправление .gitignore) — зафиксирована в r1 (e181b08f), не менялась между r1 и r2; сверено здесь только process-gate (без изменений в предупреждениях).
  • demo/performance/card-contract.mjs, test/performance-contract.test.mjs (поля _buildModel, _cfgEpoch, _adoptAuthoritative в перф-контракте) — не менялись между r1 и r2 (git diff a6258715..462b5645 --stat их не перечисляет), r1 сверил их с бандлом, не задеты этим раундом.
  • Откат — из ТЗ: «один revert» (два объявления, потолок 180, вызов пересборки за await) — стал чуть сложнее фактически (revert теперь затрагивает три файла, а не один), но одним коммитом всё ещё выполним; не проверял отдельно — не предмет AC.

Чего не проверял

  • npx tsc --noEmit, npm test (весь набор), npm run build третьим прогоном — Validate зелёный на 462b5645 (https://github.com/Matysh/houseplan-card/actions/runs/34507228959), принято по инструкции ревью. Точечно перегонял только изменённые тестовые файлы (config-adoption*.test.mjs, bundle-assets.test.mjs) и мутанты — см. «Как проверялось».
  • HA-харнесс (tests_backend/test_ha_*.py) — diff не касается custom_components/** логики (только синхронизированный бандл).
  • npm run invariants — diff не трогает рёбра комнат, layout, marker.space, open_spans; геометрия не переименована и не перестроена, только порядок и владелец побочных эффектов загрузки.
  • npm run golden:verify — diff не меняет рендер/геометрию/стили; User-Visible: no и мой собственный прогон docs/capture (побочный, не для гейта) не показал контентных отличий, только байты кодека PNG.
  • 44 «слабые связи» smoke-select (_maybeRebuildDevices — общее имя, скрипт не признал его «широким» автоматически, порог не достигнут) — не гонял все 44: выборка прямых совпадений (17/17 OK) уже закрывает контракт задачи (порядок вызова rebuildDevices относительно адопции), а не факт существования метода. Решение ревьюера, не обязанность.
  • Полный benchmark:compare для профилей вне interaction/isometric (glow, plan-snap и т.д.) — вне AC4/AC5, диф их не касается.
  • --issues проверка process-gate.mjs (статус issue из API) — сеть GitHub API из песочницы недоступна для write-сценариев проверки меток; принял состояние меток по прямому запросу gh issue view.

Гейты: что прогнал сам и почему

Гейт Прогнан Результат
node scripts/mutation-gate.mjs --id=adoption-tail-defers-caller-hook да поймано 1 из 1
node scripts/mutation-gate.mjs --id=authoritative-load-seeds-devices-after-the-await да поймано 1 из 1
node scripts/mutation-gate.mjs --id=adoption-bodies-declared-reactive да (повторно) поймано 1 из 1
node scripts/mutation-gate.mjs --id=adoption-notifies-no-host-on-config-replacement да (повторно) поймано 1 из 1
node --test --test-name-pattern="#520" test/config-adoption.test.mjs test/config-adoption-ownership.test.mjs да 6/6 pass
node --test test/bundle-assets.test.mjs да 26/26 pass
node scripts/process-gate.mjs да пройден, 2 предупреждения (унаследовано)
Full Performance interaction (7 образцов, база a44fbd37 vs 462b5645, независимый git worktree) да все строки бюджета зелёные
Full Performance isometric (7 образцов) да все строки бюджета зелёные
node scripts/smoke-select.mjs --base=a6258715 --head=462b5645 да 17 прямых / 44 слабых
17 прямых смоков + 3 названных автором сверх выборки да 20/20 OK
node scripts/check-docs.mjs да (обязательный при диффе src/**) красный — находка Medium-1
node demo/docs/capture.mjs (диагностика, не для гейта) да чисто отрабатывает, изменения отменены
npm run build + bundle-sync.mjs да (нужно для бенчмарка) чисто, три копии совпадают
npx tsc --noEmit, npm test (весь набор) нет Validate зелёный на этом SHA, принято
npm run invariants, golden:verify, HA pytest нет geometry/render/backend не тронуты
44 «слабых» смока, остальной полный набор нет вне обязательного объёма для этой дельты

Вердикт

Жёлтый. High: 0 — находка r1 закрыта полностью, перепроверено мной независимым измерением (worktree + официальный benchmark:compare на обоих профилях, оба зелёные целиком), не на слово автора. Medium: 1, в скоупе — node scripts/check-docs.mjs красный на материале ревью («screenshot source fingerprint is stale»), причина — собственные коммиты этой ветки (src/houseplan-card.ts, src/config-adoption.ts), подтверждено сравнением с origin/dev (зелёный там же). Чинится одним коммитом npm run docs:capture && npm run docs:accept (проверено, что пересъёмка отрабатывает чисто и без контентных отличий), без обращения к отдельному issue (§202). Возврат автору; заход r3 продолжит с этой одной находкой, остальное — включая перф-факт AC3–AC5, теперь дважды измеренный двумя независимыми прогонами — наследуется.


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

  • Ветка: issue/520-first-frame, коммит 462b56453cd6948c083e62ad8c92ec29aeb022b2.
  • Дерево материала: d7de184623e6a2759c67f56816b78f38166eb183.
  • Тело issue на момент ревью: sha256 4524f04412... (см. «Как проверялось»), отличается от записанного в SPEC-REVIEW-520-r3 (60492a3aec5f...) — находка по #517, разобрана в тексте документа, возврата в spec не требует.
  • Вердикт этого документа: жёлтый · High 0 · Medium 1 (в скоупе)

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

  • Ветка: issue/520-first-frame, коммит 462b56453cd6 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: d7de184623e6a2759c67f56816b78f38166eb183
    git log --all --format='%H %T' | grep d7de184623e6
    
  • Тело issue: e27aabb67c6fac2d56a562dfb03405fb232615db4790506469cc89ee5993501c
  • Вердикт конвейера: yellow · High 0