31 KiB
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, а
не смена скоупа.
- Прочитан весь диф
git diff a6258715..462b5645(r1→r2) и общийgit diff origin/dev...HEADцеликом (не только имена файлов). - Прочитан
src/config-adoption.ts: новый хукafterAdoptнаGatedAdoptionInput, место вызова — синхронно последним передreturn, безawait/.then(между усыновлением и хуком (строки 401-412, 428-459). Единственный предшествующийawait(host._signer.prepareImage) стоит ДОadoptStructuralResponses, т.е. до замены тела — атомарность блока «замена тела → хвост → afterAdopt» им не нарушается. - Прочитан
src/houseplan-card.ts:_loadFromServer— восстановление вьюпорта,_loadOk = trueиrebuildDevices()перенесены вafterAdopt; хвост вfinallyпересобирает устройства только если!devicesRebuilt(переменная замыкания, ровно один рестарт на попытку)._reloadConfigOnly— та же схема, короче. - Прочитаны оба новых теста:
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(между.
- Прочитаны оба новых мутанта (
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-комментария. - Находка 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-измерений автора. npm run buildи синхронизация трёх копий бандла — прогнаны в рамках п.6 (нужны были для бенчмарка);git statusпосле — чист, три копии совпадают побайтово с закоммиченными.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(см. «Гейты»).node scripts/check-docs.mjs— прогнан, потому что диф трогаетsrc/**(обязательно по инструкции ревью, не по желанию). Красный — см. «Находка Medium-1».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», а не переверку всех двенадцати.- Бюджет бандла: прочитан диф
scripts/bundle-budget.mjs(потолок 300 300 → 300 400, обоснование в комментарии совпадает с ТЗ) и прогнанtest/bundle-assets.test.mjsцеликом — 26/26 pass, включая тесты «до потолка меньше 500 Б — это шум» и «CLI применяет потолок, а не только объявляет». - Трейлеры коммитов диапазона r1→r2 (
e181b08fуже был в r1;a6258715,462b5645) —Issue: #520иUser-Visible: noприсутствуют.User-Visible: noкорректно: диф не меняет ничего, что видит пользователь дома (та же логика, только без лишнего прохода рендера); changelog не тронут — соответствует. 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 —
firstStableRenderMs2759.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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
d7de184623e6a2759c67f56816b78f38166eb183git log --all --format='%H %T' | grep d7de184623e6 - Тело issue:
e27aabb67c6fac2d56a562dfb03405fb232615db4790506469cc89ee5993501c - Вердикт конвейера:
yellow· High 0