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

99 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://github.com/Matysh/houseplan-card/actions/runs/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-раннере.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/627-lazy-admin-locales`, коммит `40959feb1d44` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `5210484960ba796270c76eaf2125b8ff037af8a5`
```
git log --all --format='%H %T' | grep 5210484960ba
```
- Тело issue: `11ffae080ad26691a4b8dbd32f7736895c0c25a3f0cabe1b09da0e21666e6562`
- Вердикт конвейера: `yellow` · High 0