Files
houseplan-card/docs/reviews/CODE-REVIEW-627-r1.md
T
2026-09-24 14:51:19 +03:00

21 KiB
Raw Blame History

CODE-REVIEW-627-r1

Материал: 40959feb1d44e51d653f8c6e9cdd5caa7f9ad9e8 (origin/dev..HEAD — один коммит), дерево 5210484960ba796270c76eaf2125b8ff037af8a5. Рабочая копия уже на этом SHA; git fetch/checkout не выполнялись. Заход r1, блокирующих циклов израсходовано 0 из 4 (полный трек: лимит 4).

Скоуп

ТЗ #627 (полный трек): английский слой словарей settings/support/topology остаётся статическим, ru/de/fr выносятся в девять ленивых чанков (пространство × язык); загрузчики онбординга, редактора и Zigbee-оверлея ждут «оседания» своего словаря перед установкой поверхности; составной гейт хоста (composeLanguageRuntimes) учитывает словари загруженной на хосте поверхности при смене языка на лету; bundle-manifest.mjs/bundle-budget.mjs получают новый граф lazyNamespaceLocaleFiles и потолок LAZY_ONBOARDING_GZIP_CEILING, редакторский потолок пересчитан. Диапазон: класс A (src/i18n/**, houseplan-card.ts, рантаймы) + B (скрипты бандла, тесты, смок, реестр мутантов) + D (пересобранный бандл, три копии).

Материал ревью совпадает по содержанию с описанием в хендоффе автора (комментарий от 2026-09-24T04:23:43Z) и итоговым возвратом на S7-code-review после ребейза на ушедший вперёд dev (комментарий 05:04:50Z, материал переподписан на a176c4b2 → затем ещё раз перебазирован на актуальный dev до 40959feb, что и дало другой хэш дерева/части файлов вроде scripts/mutation-registry.mjs — содержимое диффа origin/dev...HEAD, которое я разбираю ниже, при этом полностью совпадает с тем, что описано в хендоффе).

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

Дешёвые гейты подтверждены зелёным Validate на этом самом SHA (run 35958661751, success): typecheck, npm test (юниты), npm run build+bundle:sync, мутанты по диффу (6/6 шардов) — все success; smoke/golden/performance_smoke/backend/hacs/hassfest/geometry_parity в этом прогоне skipped (не heavy), поэтому браузерные смоки и golden я обязан проверить сам.

Прогнано лично на этом SHA:

Гейт Команда Результат
Typecheck + build npm run build ok, byte-identical dist (после bundle-sync — git status чистый)
Bundle sync + budget node scripts/bundle-sync.mjs && node scripts/bundle-budget.mjs initial View 289603 (потолок 290400±2000), lazy editor 227869 (потолок 228900±2000), lazy onboarding 27470 (потолок 28500±2000), lazy namespace locales 20867 (9 чанков) — совпадает с числами хендоффа
check-docs node scripts/check-docs.mjs --screenshots=warn ok (7 файлов, 12 внешних ссылок); фингерпринт скриншотов помечен WARN (стало ещё до этой задачи, это предбета-долг #479, не находка)
check-inputs node scripts/check-inputs.mjs --coverage пусто (0), как заявлено
Юнит-тесты изменённых файлов node --test test/i18n-lazy-namespaces.test.mjs test/i18n-runtime.test.mjs test/zigbee-topology.test.mjs test/bundle-assets.test.mjs 4+9+25+36 = 74 pass, 0 fail
Мутанты (все 5 новых + 2 с обновлёнными якорями) node scripts/mutation-gate.mjs --id=<id> ×7 все 7 «поймано 1 из 1»: onboarding-loader-skips-namespace-ensure, bundle-budget-onboarding-ceiling-dropped, i18n-namespace-eager-locale, locale-gate-ignores-namespaces, namespace-loader-returns-english, danger-confirm-uses-last-rendered-language-gate, (топология — см. ниже)
Смоки: прямые совпадения smoke-select (8) node demo/smoke_{danger_confirmation,help_affordance,houseplan_panel,lazy_admin_locale,lazy_editor_chunk,pdf_export,post_write_adoption,support_feedback}.mjs 7 OK, 1 упал — см. находку M1
Смоки: зарегистрированные связи smoke-select (3) node demo/smoke_{entry_stale,french_locale,zigbee_topology_hover}.mjs все 3 OK
Смоки, названные в AC регресс-списке, не входящие в прямые/связи smoke_general_settings_form, smoke_room_settings_form, smoke_device_settings_form, smoke_space_settings_form, smoke_version_recovery, smoke_german_locale все 6 OK
Новый смок AC4–AC7 node demo/smoke_lazy_admin_locale.mjs OK, все 38 полей true

node scripts/smoke-select.mjs --base origin/dev --head HEAD: 8 прямых совпадений, 3 зарегистрированные связи, 45 слабых (одно распространённое имя _config/_editorRuntime) — по правилу промпта слабые не обязательны к прогону; я прогнал дополнительно те AC-именованные смоки из плана автотестов, что не попали в прямые/связи (список выше), итого разобраны все 14 смоков, названных в плане автотестов ТЗ, плюс новый.

Находки

M1 (Medium, в скоупе задачи) — demo/smoke_danger_confirmation.mjs не переведён на ожидание составного гейта; диалог опасного действия может быть незаметно потерян при переключении языка

Файлы: demo/smoke_danger_confirmation.mjs (не тронут этим коммитом — единственный из смоков, синхронно переключающих язык поверх открытого редактора/онбординга, который автор пропустил в списке из девяти переведённых на ожидание гейта); причина в src/houseplan-card.ts:1968-1973 (_syncDangerConfirmLocaleGate) и src/houseplan-card.ts:4033-4036 (авто-отмена на warm, #417).

Дефект: _syncDangerConfirmLocaleGate теперь берёт составной рантайм загруженной поверхности (_editorRuntime?.languageRuntime ?? _onboardingRuntime?.languageRuntime ?? LANGUAGE_RUNTIME), а не только основной каталог. Существующий с #417 механизм «отменить _dangerConfirm, если гейт вернул warm» из-за этого расширения начинает срабатывать и в момент, когда основной каталог уже осел (тест ждёт именно это условие: card._t('btn.cancel') !== expectedCancel), а словарь пространства (settings) на этом же хосте — ещё нет: составной гейт остаётся pending → рендер-цикл видит warm (хост уже был committed на английском) → _cancelDangerConfirm() срабатывает синхронно, до того как диалог успевает отрисоваться. Вызывающий код получает false без единого кадра диалога и без какой-либо видимой подсказки — не «английская вспышка», которую ТЗ прямо предвидело в разделе «Риски», а тихая потеря запроса подтверждения.

Воспроизведение:

  1. npm run build && node scripts/bundle-sync.mjs.
  2. node demo/smoke_danger_confirmation.mjs — падает необработанным исключением внутри localeFits('de', …): TypeError: Cannot read properties of undefined (reading 'click') (воспроизвелось на первом прогоне в чистой сессии; из ~8 последующих прогонов подряд — 0 падений, гонка зависит от относительной скорости загрузки чанков de.ts и settings-de-*.js).
  3. Детерминированное воспроизведение (не закоммичено, только для диагностики): в копии смока добавлена задержка 40 мс на ответ **/settings-de-*.js* через page.route перед вызовом localeFits('de', 'Abbrechen') — падение стабильно повторяется на каждом прогоне.
  4. Контрольный прогон на origin/dev (22c7d634, до #627) в отдельном воркчасти: тот же смок проходит зелёным («OK», все поля true) — регрессия точно привязана к этому диффу, а не к окружению.

Чем краснеет (защитное свойство «диалог опасного действия не теряется при переключении языка поверхности»): задержка ответа любого из девяти namespace-чанков (settings/support/topology × ru/de/fr) относительно основного каталога — продемонстрировано выше для settings-de.

Почему Medium, а не High: прямой угрозы безопасности нет — потерянный запрос разрешается в false («не подтверждено»), ни одно деструктивное действие не выполняется без подтверждения; в реальном UI хост становится inert на время warm, что обычно блокирует пользовательский клик в этом же окне. Но это реальная, воспроизводимая регрессия существующего регресс-смока (он попал в «прямое совпадение» инструмента smoke-select и напрямую относится к вводимому этой задачей контракту), она интермиттентна и не поймана ни одним мутантом (гвард danger-confirm-uses-last-rendered-language-gate — другой файл, smoke_danger_confirm_branches.mjs, который эту гонку не задевает) и ни одним юнит-тестом; оставленная как есть, она будет время от времени красить полный смок-прогон перед бетой в зависимости от таймингов сети/чанков — именно то, что процесс просит ловить на этапе код-ревью, а не на релизном гейте.

Почему в скоупе, а не отдельным issue: ломающий смок и вызвавшая его причина — прямое следствие ядра этой задачи (составной гейт по словарям пространств), тот же риск, что ТЗ называет в разделе «Риски» («смоки… нужно перевести на ожидание гейта, а не ослаблять проверки»); чинится он тем же приёмом, что уже применён к девяти другим смокам в этом коммите.

Вердикт по AC: не относится ни к одному конкретному AC1–AC7 напрямую (это не то поведение, которое AC описывают), но затрагивает общий контракт задачи «поверхность не мигает английским / не виснет» — в данном случае «не молчит вместо диалога».

Low/иных находок нет.

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

AC Проверено Итог
AC1 bundle-budget.mjs печатает и гейтит lazy onboarding тем же lazyGraphCeilingViolation; реальный манифест и синтетика (test/bundle-assets.test.mjs, тесты «#627 AC1», «#593») — прогнано лично, зазор >500 Б с обеих сторон подтверждён числами (28500−27470=1030 сверху, 27470−(28500−2000)=970 снизу); мутант bundle-budget-onboarding-ceiling-dropped красит целевой тест Доказан
AC2 Числа воспроизведены лично (bundle-budget.mjs): онбординг 27470 ≤ 28000 и 34526−27470=7056 ≥ 6500; редактор 244433−227869=16564 ≥ 15000 (запас к новому потолку 228900±2000 = 1031/969, оба причины записаны в комментариях к константам); initial View 289603−289449(ориг. измерение из issue)=154 ≤ 300, в полосе потолка (290400±2000, запас 797/1203) Доказан
AC3 Девять чанков ровно, ни один не пересекается с initial/editor/onboarding — проверено чтением bundle-budget.mjs/bundle-manifest.mjs + прогоном test/bundle-assets.test.mjs (владение по содержимому, отказы графов) и нового test/i18n-lazy-namespaces.test.mjs (нет статических импортов ru/de/fr, у каждого загрузчик с двумя попытками, ровно один retry-токен) — оба файла прогнаны лично, 100% pass; мутант i18n-namespace-eager-locale красит Доказан
AC4 Холодное открытие онбординга (ru), «Общих настроек» (de), диалога устройства (fr): первый кадр диалога на языке пользователя, задержка чанка 250 мс скрыта под существующим индикатором, ни английской строки, ни сырого ключа ни в одном кадре — прогнан smoke_lazy_admin_locale.mjs лично, все соответствующие поля true; мутант onboarding-loader-skips-namespace-ensure красит именно этот смок (проверено запуском) Доказан
AC5 Live-переключение ru→de при открытых «Общих настройках»: прежний кадр удерживается (inert+aria-busy), финальный кадр — целиком немецкий, чанк запрошен один раз, вторая карточка не запрашивает повторно — тот же смок + test/i18n-runtime.test.mjs «#627 AC5» (прогнан лично, включает случай отказа только пространства без падения lang в en); мутант locale-gate-ignores-namespaces красит Доказан. См. M1 — соседнее поведение (danger-confirm) той же самой compound-гейт-логики страдает в узком гоночном окне, которое AC5 не описывает и не покрывает
AC6 Отказ обеих попыток чанка → английский только в словаре этого пространства, один тост, один console.warn, диалог не виснет — прогнан смок лично, поля true Доказан
AC7 Холодный View (en/ru) не запрашивает ни одного namespace-чанка; en-админ в редакторе — тоже; Zigbee-слой на ru запрашивает только topology-ru один раз — прогнан смок лично (журнал запросов), плюс smoke_zigbee_topology_hover.mjs на ru отдельно — оба зелёные Доказан
Владение графов (assertSupportBundleOwnership/assertNamespaceLocaleOwnership) Проверено чтением + прогоном bundle-budget.mjs как CLI на реальном dist/houseplan-assets.json — не бросает Корректно
Трейлеры Issue: #627, User-Visible: no — на единственном коммите; видимое поведение действительно не меняется (сами тексты/ключи не тронуты, изменился только механизм доставки), changelog обоснованно не редактировался Корректно
Одно число — один источник LAZY_EDITOR_GZIP_CEILING/LAZY_ONBOARDING_GZIP_CEILING объявлены один раз в scripts/bundle-budget.mjs, тесты импортируют константы, а не дублируют литералы; NAMESPACE_LOCALE_CHUNKS (9) — тоже единственный источник, импортируется тестами и смоком Корректно
monolith-baseline.json bundleBytes 2 500 387 → 2 508 676 Причина записана (десять новых чанков × rollup intro/export), пересчитано и сошлось с фактической сборкой Корректно
tsconfig.test.json / CONTRIBUTING.md Три новых модуля включены в тестовую компиляцию; абзац про namespace-словари добавлен по существу Корректно

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

  • golden:verify и перф-смок взаимодействия — оба требуют Linux CI/предбета-гейт; эталоны не менялись, автор обоснованно ожидает отсутствие изменений кадров (рантаймы ждут словарь до установки, а не меняют разметку) — не проверял сам, доверяю рассуждению, но это не заменяет обязательный прогон на кандидате.
  • Полный реестр мутантов (только диффовые 6/6 шардов подтверждены Validate; ночной mutation-gate.yml не запускал).
  • ~250 смоков вне прямых совпадений/связей/AC-плана — не прогонял (широкий охват — предрелизная обязанность, не гейт ревью).
  • pytest tests_backend — Python не менялся, не запускал.
  • Живой стенд Home Assistant — не поднимал.
  • Perf-смок взаимодействия по диффу houseplan-card.ts — по правилам гейтится heavy-профилем CI, не гейт код-ревью; не запускал.

Материал для повторного раунда

Если находка M1 будет закрыта правкой demo/smoke_danger_confirmation.mjs (по образцу уже переведённых девяти смоков — ждать составной гейт/aria-busy перед вызовом _confirmDanger, а не только card._t('btn.cancel')), разбор r2 может ограничиться: диффом этого файла + одним прогоном (в идеале с искусственной задержкой namespace-чанка, как в этом отчёте) для доказательства, что гонка закрыта, а не просто перестала проявляться на быстром CI-раннере.


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

  • Ветка: issue/627-lazy-admin-locales, коммит 40959feb1d44 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 5210484960ba796270c76eaf2125b8ff037af8a5
    git log --all --format='%H %T' | grep 5210484960ba
    
  • Тело issue: 11ffae080ad26691a4b8dbd32f7736895c0c25a3f0cabe1b09da0e21666e6562
  • Вердикт конвейера: yellow · High 0