18 KiB
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 коммита:
e8916c71feat: restore direct device add shortcut —User-Visible: yes3068a97ctest: align device catalog evidence with shortcut5ba0d9e1test: accept reviewed device shortcut frames — golden baselinese5ecf399chore: 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 («Переиспользование: дерево уже проверено»), но проверил цепочку: они реально выполнялись и были зелёными на run33246023321(headShaa043c216, дерево идентично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 не требуется.