# 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= --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