# CODE-REVIEW-363-r2 ## Метаданные раунда — почему это «r2», но полный разбор Заголовок задачи фиксирует «заход r2 · блокирующих циклов израсходовано 1 из 2» для этапа **code**. Проверка меток issue (`gh api .../issues/363/timeline`) показывает, что `S7-code-review` выставлена **впервые** в 09:49:41 — до этого не было ни одного `S7-code-review → S6/S7` цикла. Комментариев с вердиктом код-ревью в issue тоже нет (всего 5 комментариев, все относятся к аналитике S2 и двум попыткам этапа spec). Единственный потраченный блокирующий цикл (1/2) — это жёлтый вердикт **спек-ревью** r1 (2026-08-29T09:16:10Z, `SPEC-REVIEW-363-r1.md@9d6d6eb9`), исправленный в той же попытке до зелёного (09:22:49Z). Бюджет §4 общий на issue, а не на этап, поэтому нумерация «заход» продолжает расти при переходе в code, хотя это первый разбор кода. **Следствие:** предыдущего code-review раунда, с которым можно было бы сравнивать дельту (§2.9), не существует. Материал этого раунда — весь диапазон `git diff origin/dev...HEAD` (4 коммита реализации), разобран полностью. Разделы «Закрытие раунда r1» и «Унаследовано из r1» в этом документе отсутствуют по той же причине: наследовать не от чего. ## Скоуп проверки Диапазон: `origin/dev..HEAD`, 4 коммита: - `e8916c71` feat: restore direct device add shortcut — `User-Visible: yes` - `3068a97c` test: align device catalog evidence with shortcut - `5ba0d9e1` test: accept reviewed device shortcut frames — golden baselines - `e5ecf399` chore: preserve changelog mode after rebase (только смена прав доступа файла, содержимое не менялось) Трейлеры `Issue: #363` присутствуют во всех коммитах, `User-Visible` расставлен корректно (`yes` только там, где меняется видимое поведение и в этом же коммите правится оба changelog). ## Как проверялось Прочитаны в указанном порядке: `docs/SCOPE.md`, `AGENTS.md`/`PROCESS.md`, тело issue #363 (ТЗ revision 2) и все 5 комментариев, `docs/USER-GUIDE(.ru).md` (раздел Device editor), исходный код (`src/houseplan-editor-runtime.ts`, `src/houseplan-card.ts`), тесты (`demo/smoke_editor_tabs.mjs`, `demo/smoke_device_inbox.mjs`, `test/i18n.test.mjs`), golden-матрица (`demo/golden/matrix.mjs`, `demo/golden/baselines/baselines-index.json`), `docs/images/screenshots.json`, история коммита `cab8d128^` (состояние до удаления кнопки), CSS тулбара (`src/styles/chrome.styles.ts`), а также журнал запусков workflow через `gh run view`/`gh api` (Validate на `e5ecf399`, докрышоты на ref `issue/363-add-device-shortcut`). ## AC-за-AC **AC1 — видимость и порядок.** Подтверждено чтением: `src/houseplan-editor-runtime.ts:11541-11555` — `_renderDevicesBar()` рендерит кнопки в порядке «Добавить» (иконка `mdi:plus-box-outline`) → «Устройства» → «Правила иконок»; `_renderDevicesBar` вызывается только при `editorChromeMode === 'devices'` (`houseplan-card.ts:11066-11072`), в других режимах не рендерится вовсе. Доказано динамически: `demo/smoke_editor_tabs.mjs` проверяет `deviceToolbarButtons.length === 3`, что нулевая кнопка — Add (текст, title, иконка), первая — Devices. Тест умеет падать: смена порядка, иконки или подписи ломает соответствующее булево поле, которое `checkAll` требует истинным по умолчанию. **AC2 — прежний диалог.** `_openMarkerDialog()` без аргумента (`houseplan-editor-runtime.ts:7628,7721-7745`) создаёt черновик с `name: '', bindingMode: 'virtual'`, без ключа `devId` (ветка `else`). Смок кликает по кнопке и проверяет ровно эту форму (`!Object.prototype.hasOwnProperty.call(c._markerDialog, 'devId')`) — мутант «вызов с аргументом» ловится: при `d` передан объект `devId` появится. Клавиатурная семантика (Enter/Space) не эмулировалась синтетическим `KeyboardEvent`, а подтверждена структурно (`addDeviceButton.tagName === 'BUTTON' && !addDeviceButton.disabled`) — нативная кнопка получает Enter/Space активацию от браузера бесплатно; это тот же способ, каким уже проверена соседняя кнопка «Устройства». Отдельный тест на сам факт срабатывания нативной семантики тестировал бы браузер, а не код продукта — не считаю это пробелом. **AC3 — каталог сохранён.** `demo/smoke_device_inbox.mjs` не тронут в части самого каталога: `_openDeviceInbox()`, 4 вкладки, ряды `on_plan`, поиск по комнате — без изменений. Единственная правка — переименование поля `oneCatalogEntryPoint` → `catalogAndDirectAddEntryPoints` и добавление позитивной проверки, что кнопка `^Add$` **есть** (ранее было наоборот, из контракта #29). Путь «Добавить виртуальное устройство» (`device_inbox.add_virtual`) не задет диффом вообще — регресс здесь структурно исключён (нет изменённых строк). **AC4 — i18n.** `src/i18n/{en,ru,de}.json` — по 2 строки в каждом. Сверено побайтово с `git show cab8d128^:src/i18n/{en,ru}.json` — EN/RU совпадают дословно. Немецкий («Hinzufügen», «Gerät zum Plan hinzufügen») согласован с уже существующими `device_inbox.add`/`title.add_space` в том же словаре. `test/i18n.test.mjs:109-112` сверяет полный набор ключей всех словарей с `en` — пропуск любой локали или ключа ломает тест (assert.deepEqual на отсортированных ключах), проверено чтением, дополнительных прогонов не требовалось — тест общий, не требовал правки под #363. **AC5 — responsive и доступность.** `.editbar` использует CSS-grid (`minmax(0,1fr) auto`, `chrome.styles.ts:193-214`) — колонка `.editbar-end` (Close) зафиксирована отдельной grid-колонкой и не участвует в переносе; `.editbar-tools` — `flex-wrap: wrap`, кнопки переносятся, а не обрезаются и не перекрываются. **Этот CSS не тронут диффом** — риск переполнения тулбара, заявленный в ТЗ, снят тем же механизмом, что уже работает для соседних кнопок, без новой логики. Прямого browser-смока «ширина X, нет overlap» именно для голого тулбара (без диалога) не добавлено, но принятый golden-кадр `device-inbox-narrow-ru-dark` (390px) визуально показывает тот же тулбар (приглушённым) под диалогом и был вручную проверен автором при приёмке — косвенное, но реальное подтверждение при самой узкой из используемых в матрице ширин. Считаю риск закрытым; отдельный узкий смок был бы полезен, но не обязателен при неизменном layout-механизме — не поднимаю как находку. **AC6 — данные и логика неизменны.** Диф не касается persisted-схемы, save/cancel flow, backend. Новый draft создаётся той же веткой `_openMarkerDialog()`, что и раньше для «пустого» диалога — нет второго create-flow. Cancel не создаёт marker (ветка `else` только формирует локальный `_markerDialog`, запись в `_serverCfg`/`callWS` происходит по существующему save-пути, не тронутому диффом). **AC7 — документация и выпуск.** Оба changelog правятся в `e8916c71` (`User-Visible: yes`), USER-GUIDE обновлён в EN и RU в том же коммите. Гейт `check-docs.mjs`: `docs/images/screenshots.json` — `sourceFingerprint` обновлён для всех 10 сценариев, `imageSha256` изменился **только** у `device-editor` (7 байт разницы в PNG, автор задокументировал это как antialiasing-флуктуацию инфраструктуры при кросс-сверке 7 прогонов workflow — правдоподобно и не относится к продукту). Канонический workflow «Скриншоты документации» запущен на `ref: issue/363-add-device-shortcut` (проверено логом job `actions/checkout@v7` рана `33245780081` — вопреки `headBranch: main` в метаданных API, что для этого репозитория штатно: workflow-файл живёт на `main` побайтово синхронизированным с `dev` (#365), а checkout идёт по input `ref`). `docs:accept --reviewed` выполнен, обновлённый `docs/images/screenshots.json` закоммичен. **AC8 — визуальный эталон.** `baselines-index.json`: изменились ровно два хэша — `geometry-devices-editor-dark` и `device-inbox-narrow-ru-dark`; `device-inbox-desktop-en-light` и `device-inbox-desktop-ru-dark` не менялись. Здесь есть реальное расхождение с буквальным текстом AC8: ТЗ утверждает, что все три `device-inbox-*` не меняются и называет единственный `--expect-change=geometry-devices-editor-dark`. По факту `device-inbox-narrow-ru-dark` тоже сменил хэш (102015→103240 байт PNG) — и это ожидаемо: при ширине 390px приглушённый тулбар виден под диалогом каталога, и новая кнопка «Добавить» видна в этом приглушённом фоне. Автор явно задокументировал и обосновал это в комментарии к issue до приёмки, само изменение содержимого каталога не задело. **Квалифицирую как Low, не блокирует**: причина понятна, зафиксирована публично, поймана предсказанным самим же спек-ревью механизмом («если поедут — находка»), пиксельная дельта согласуется с размером одной кнопки. Рекомендация — одной строкой поправить текст AC8 в issue (перечислить `device-inbox-narrow-ru-dark` в ожидаемо меняющихся вместе с `geometry-devices-editor-dark`), но это не повод для возврата автору. ## Гейты: что прогнано и что нет, и почему - `npx tsc --noEmit`, `npm test`, `npm run build` (+ сверка 3 копий бандла) — **не прогонял сам**: Validate на `e5ecf399` зелёный целиком (https://github.com/Matysh/houseplan-card/actions/runs/33246304275, job «Фронтенд: типы, юниты, мутанты, синхрон бандла» = success). - `node scripts/check-docs.mjs` — тот же Validate-прогон покрывает job «Предполётные проверки: документация, провенанс, процесс» = success; дополнительно проверил содержимое `screenshots.json` вручную (см. AC7). - Полный набор golden (`npm run golden:verify`) и все 3 шарда browser-смоков — на самом `e5ecf399` эти джобы **skipped** («Переиспользование: дерево уже проверено»), но проверил цепочку: они реально выполнялись и были зелёными на run `33246023321` (headSha `a043c216`, дерево идентично `e5ecf399` за вычетом смены прав доступа файла) — «Golden-кадры против принятых эталонов» = success, все 3 шарда смоков = success. Повторный прогон не требовался. - `node scripts/smoke-select.mjs --base origin/dev --head HEAD` — прогнал. Инструмент не нашёл «прямого совпадения» или «зарегистрированной связи»: вернул НЕОПРЕДЕЛЁННОСТЬ и список из 28 слабых связей (все — смоки, вызывающие `_openMarkerDialog`, распространённое имя). Решение: не прогонять — этот диф не меняет ни сигнатуру, ни поведение `_openMarkerDialog()`, а только добавляет ещё один вызов без аргумента (уже покрытый паттерн, например `demo/smoke_new_device.mjs`); целевые смоки для самой фичи (`smoke_editor_tabs.mjs`, `smoke_device_inbox.mjs`) уже обновлены автором и зелены в Validate. - `python -m pytest tests_backend -q` — не запускал, диф не касается `custom_components/**/*.py`. - Инварианты модели (`npm run invariants`) — не запускал, диф не касается геометрии, стен, layout, `marker.space`, `open_spans`. - Perf-профили — не требуются ни AC, ни диффом; job перф-смока в обоих релевантных прогонах либо success, либо skipped по классификации файлов. - «Одно число — один источник» — в диффе нет новых пользовательских числовых величин (только текстовые подписи/tooltip), проверка неприменима. ## Что проверено и корректно - Порядок, иконка, подпись и tooltip кнопки — по коду и по смоку. - `_openMarkerDialog()` без аргумента — тот же прежний путь, каталог не открывается. - i18n-паритет трёх локалей, байт-в-байт совпадение EN/RU с состоянием до `cab8d128`. - Каталог и путь «Добавить виртуальное устройство» не затронуты. - Changelog/USER-GUIDE обновлены в нужном коммите на обоих языках. - Docs-фингерпринт и golden-баselines обновлены целиком согласованным набором, посторонних дрейфов не найдено. - Docs-скриншоты сняты на верном ref ветки (проверено логом job, а не метаданными API). ## Находки **L1 (Low).** Текст AC8 в теле issue #363 предсказывает, что `device-inbox-narrow-ru-dark` не изменится; по факту эта golden-сцена изменилась (ожидаемо и обоснованно — приглушённая кнопка тулбара видна под диалогом на узкой ширине). Причина понятна, изменение задокументировано автором в issue-комментарии до приёмки, риск регрессии отсутствует. Рекомендация: точечно поправить формулировку AC8, не блокирует. Находок Medium/High нет. ## Вывод Диф полностью реализует ТЗ revision 2, AC1–AC8 доказаны кодом/тестами/CI, трейлеры корректны, changelog и docs synced. Единственное замечание — Low, снимается с запиской, отдельный issue не требуется.