Files
houseplan-card/docs/reviews/CODE-REVIEW-363-r2.md
T
2026-08-29 10:01:16 +00:00

18 KiB
Raw Blame History

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 не требуется.