Files
houseplan-card/docs/reviews/CODE-REVIEW-406-r1.md
T
2026-09-01 16:32:25 +00:00

19 KiB
Raw Blame History

CODE-REVIEW-406-r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/406
  • Этап: code (PROCESS.md §2.7)
  • Заход: r1 · блокирующих циклов израсходовано 0 из 4 (перед этим раундом)
  • Ветка: issue/406-beta2-polish
  • Коммит на ревью: fe385cbf9d5a0fc11f0bcfacbe4ba03e01167949
  • ТЗ: docs/specs/406-beta2-polish.md, revision 4, зелёный SPEC-REVIEW-406-r4
  • База сравнения: origin/dev = b04387ce914aa8559fb3bc446f99a5751f67f32c

Скоуп

Четыре независимые правки из ТЗ: (а) удаление 13 мёртвых ключей i18n + AST-гейт test/i18n-dead-keys.test.mjs; (б) role="alertdialog" + aria-describedby для hp-confirm (destructive и warning), обычные диалоги остаются на HA-ветке (src/hp-dialog.ts, src/hp-confirm.ts); (в) смок demo/smoke_danger_confirm_branches.mjs покрывает и HA-, и нативную ветку; (г) уборка marker_area_snapshot для исчезнувших устройств только при авторитетном реестре + разворот правила усечения по лимиту (src/device-area-relocation.ts).

Материал ревью — git log --oneline origin/dev..HEAD (9 коммитов, из них 5 docs: review document for #406/docs: resolve.../docs: revise... — все имеют отношение к ТЗ, не к коду) и git diff origin/dev...HEAD (42 файла: продуктовый код, тесты, смоки, мутанты, i18n, бандлы, changelog).

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

Ручного тестирования в цикле нет — правильность работы устанавливается чтением кода, автотестами и браузерными смоками.

Прогнано лично на fe385cbf (зелёного Validate на этом SHA нет):

Гейт Результат
npx tsc --noEmit green, без вывода
npm test 1706 passed, 0 failed, 1 skipped — совпадает с заявкой автора
npm run build green, dist собран за 16.3s
node scripts/bundle-sync.mjs --check (после build) green, три копии бандла синхронны
npm run bundle:budget initial 287320 B / budget 300000 B, headroom 12680 B (>0 → AC12 выполнен); есть предупреждение о низком запасе — это фон #367, не регрессия этой задачи
node scripts/mutation-gate.mjs --check все мутанты, включая три новых (i18n-dead-key-returns, confirm-dialog-loses-alertdialog, area-snapshot-cleanup-ignores-authority), — ok
node scripts/check-docs.mjs RED — см. находку ниже
node demo/smoke_danger_confirm_branches.mjs green, все 24 поля true (AC6–AC8)
node demo/smoke_area_relocation.mjs green, все 20 полей true, включая authoritativeOrphanRemoved/nonAuthoritativeOrphanPreserved (AC9–AC10)
node scripts/smoke-select.mjs --base origin/dev --head HEAD «НЕОПРЕДЕЛЁННОСТЬ»: изменённых символов проекта — 6 (MARKER_AREA_SNAPSHOT_LIMIT, MarkerAreaBinding, _useHaDialog, _usesHaDialog, validBinding, validText), матрица — 212 смоков, ни один не назван связанным доказуемо

Не прогонял и почему:

  • Полная матрица demo/smoke_*.mjs (212 файлов) — диапазон затрагивает только hp-dialog/hp-confirm и device-area-relocation, у обоих есть прямо названные в AC6–AC10 смоки, которые я прогнал. Символы из вывода smoke-select — внутренние помощники (_usesHaDialog, validBinding, MARKER_AREA_SNAPSHOT_LIMIT), уже покрытые юнитами test/device-area-relocation.test.mjs построчно (проверено чтением: тест на переполнение лимита обращается к MARKER_AREA_SNAPSHOT_LIMIT напрямую). Полный прогон матрицы — предрелизная обязанность (§8), не гейт этого раунда.
  • npm run golden:verify — прочитал demo/golden/matrix.mjs: ни один сценарий не открывает destructive/warning hp-confirm (есть только device-dialog-*, decor-color, optimize-preflight-dialog — другие диалоги). Видимый рендер, который затрагивает диапазон, в голден-матрице не зафиксирован — прогон не даст сигнала по этой задаче.
  • npm run invariants -- --config … — диапазон не трогает рёбра комнат, записи толщины, layout-формат, marker.space или open_spans; device-area-relocation.ts меняет только момент уборки записи снапшота и порядок среза при переполнении, не геометрию. Не применимо.
  • python -m pytest tests_backend -q — custom_components/**/*.py в диффе нет (только скомпилированные frontend-бандлы).
  • Перфоманс-профили — не названы в AC, чувствительные к перфу пути не тронуты.

Находки

Medium (в скоупе, чинится в этой же задаче) — стал устаревшим отпечаток скриншотов документации

node scripts/check-docs.mjs красный на fe385cbf:

ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs

Воспроизведение (после npm run build, который уже входит в проверки автора):

$ node scripts/check-docs.mjs
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs

Причина механическая и ожидаемая: visualFingerprint(ROOT) (scripts/source-fingerprint.mjs) считается по всему src/**, а диапазон меняет src/hp-dialog.ts, src/hp-confirm.ts, src/device-area-relocation.ts и все четыре словаря src/i18n/*.json — то есть отпечаток обязан был измениться, и docs/images/screenshots.json обязан был обновиться вместе с ним, независимо от того, меняются ли реально показанные пиксели. Я дополнительно прогнал node demo/docs/capture.mjs во временной копии рабочей дерева и подтвердил: все 10 сценариев (view-desktop, view-touch, space-create, room-contour-close, plan-context-tray, device-editor, device-display-preview, background-editor, room-card, device-info) — из demo/docs/screenshots.mjs, ни один не открывает destructive/warning hp-confirm, так что заявление автора «скриншоты не меняются» по существу, вероятно, верно (файлы отличались побайтово в моей проверке, но это из-за отсутствующего в песочнице oxipng, не из-за иного содержимого кадра — само сообщение инструмента oxipng не найден: кадры пишутся как есть, без перепаковки называет причину; я откатил эту пробную капчу перед продолжением ревью). Но идентичность пикселей не отменяет обязательности шага: docs/images/screenshots.json в диффе не тронут вообще (git diff origin/dev...HEAD -- docs/images/ — пусто), а check-docs.mjs проверяет именно факт совпадения записанного отпечатка с текущим деревом, не факт визуального совпадения.

Это ровно тот класс регрессии, который уже дважды оставлял dev с красным job docs до следующей задачи (#230, #234, #237): гейт красный на коммите, который автор передаёт на ревью, и останется красным после мержа, если его не прогнать перед мержем.

Ожидаемо: npm run build && node demo/docs/capture.mjs && npm run docs:accept (или эквивалент), обновлённые docs/images/screenshots.json и (если байты правда меняются вне зависимости от oxipng) PNG — в том же PR.

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

  • AC1–AC3 (мёртвые ключи, гейт, законные динамические/производные семейства). test/i18n-dead-keys.test.mjs строит множество через реальный TS AST (ts.createSourceFile по каждому файлу src/**/*.ts): литералы, шаблонные выражения с горизонтом +/`, и производное правило _help('x.help') → x.help.aria. Второй тест в файле жёстко фиксирует derivedHelpAria.size === 19 — то есть падает, если производных пар станет больше или меньше 19, а не просто «не меньше». src/i18n/en.json до/после: 13 ключей удалены (history.partition_add, confirm.delete_draft, confirm.delete_draft_segment, title.markup, history.delete_room, markup.delete, marker.display_hint, marker.display_hint_icon, confirm.delete_room, confirm.remove_marker, confirm.delete_space, confirm.unlock, confirm.delete_plan) — совпадает построчно со списком ТЗ. npm test включает оба теста файла и прошёл.
  • AC4 (коллизия с test/unified-wall-tool-source.test.mjs). Строка 'history.partition_add' убрана из проверяемого списка теста тем же коммитом, что и ключ из словарей — оба гейта зелёные одновременно (проверено прогоном npm test).
  • AC5 (паритет словарей). Диф по de.json/fr.json/ru.json даёт ту же тринадцатку удалённых строк, что и en.json (сверил git diff по каждому файлу построчно) — синхронно, 52 строки суммарно. test/i18n.test.mjs входит в npm test и прошёл.
  • AC6/AC7 (роль и aria-describedby). src/hp-confirm.ts: оба kind (destructive и warning) получают .alert=${true} безусловно — дихотомии по виду нет, что и требовал r1/r2 SPEC-REVIEW. src/hp-dialog.ts добавляет alert/describedBy как реактивные свойства, _usesHaDialog() возвращает false, когда alert истинен, — то есть alert-подтверждение принудительно идёт в нативную ветку независимо от customElements.get('ha-dialog'); нативный <dialog> получает role=${this.alert ? 'alertdialog' : 'dialog'} и aria-describedby=${this.describedBy || nothing}. Проверил по всему src/ (grep -n '\.alert='), что .alert выставляет только hp-confirm.ts — ни один из ~30 других вызовов <hp-dialog> (маркер, калибровка, бэкдроп, импорт/экспорт, kiosk, слияние комнат и т.д.) не трогает alert/describedBy, то есть остаются обычным role="dialog" на HA-ветке. demo/smoke_danger_confirm_branches.mjs — green, поля noHaDestructiveIsDescribedAlert/noHaWarningIsDescribedAlert/ haDestructiveStaysNativeAlert/haWarningStaysNativeAlert истинны; последняя пара доказывает именно то, что было провалом в r2 SPEC-REVIEW — alert не уходит в зарегистрированный ha-dialog. Отдельный getByRole на реальном accessibility-дереве Chromium (realAccessibilityTreeIncludesConsequence) подтверждает, что текст последствий действительно долетает до дерева доступности, а не только до DOM-атрибута.
  • AC8 (инварианты подтверждения в обоих окружениях). Тот же смок: фокус на «Отмена» (noHa*FocusesCancel/ha*FocusesCancel), Esc → false (noHaWarningEscapeResolvesFalse/haWarningEscapeResolvesFalse), клик по «Отмена» → false в обоих окружениях — все true.
  • AC9/AC10 (уборка снапшота только при авторитетном реестре). resolveDeviceAreaRelocations строит liveIds/liveBindings из options.devices и помечает removeSnapshot: true для записей снапшота, чей id и binding не совпадают ни с одним живым устройством/маркером, — но делает это только после if (!options.authoritative) return ... в начале функции, то есть цикл недостижим при неавторитетном реестре. Проверил обоих вызывающих в продукте: houseplan-card.ts:5054/5166 передаёт devices: this._devices — полный список, только что пересобранный buildDevices(...) из актуального registry/markers/settings, а не отфильтрованное подмножество; _syncAreaRelocations (запись) вызывается только при this._haRegistry.authoritative. Читающие проекции (space-render.ts:283, space-card.ts:455) используют только .relocateIds и никогда не вызывают applyAreaRelocationResolution — то есть read-only карточка не может стереть запись сама (staticMadeNoWrites: true в смоке это подтверждает). Юниты test/device-area-relocation.test.mjs («authoritative registry removes orphan snapshots but preserves live marker ids» / «non-authoritative registry preserves orphan snapshots») и смок demo/smoke_area_relocation.mjs (authoritativeOrphanRemoved / nonAuthoritativeOrphanPreserved) оба зелёные. Мутант area-snapshot-cleanup-ignores-authority (снимает условие authoritative) ловится именно этим юнитом — проверил прогоном mutation-gate.mjs --check.
  • AC11 (усечение снапшота сохраняет последние записи). Один символ правки: .slice(0, MARKER_AREA_SNAPSHOT_LIMIT) → .slice(-MARKER_AREA_SNAPSHOT_LIMIT). Новый юнит-тест строит вход LIMIT + 2 записей и проверяет, что первые две (entry-0, entry-1) исчезли, а последняя (entry-${LIMIT+1}) осталась, — тест умеет падать: на старом .slice(0, LIMIT) он утверждал бы обратное и провалился бы.
  • AC12 (бюджет initial). npm run bundle:budget → 287320 B при бюджете 300000 B, положительный запас — не растёт (совпадает с ожиданием ТЗ: удаление 52 строк уменьшает бюджет, атрибуты добавляют единицы байт).
  • Трейлеры и changelog. Коммит fe385cbf: Issue: #406, User-Visible: yes. docs/CHANGELOG.md/docs/CHANGELOG.ru.md получили по одному пункту про переход на нативную оболочку и alertdialog — в том же коммите, что и код (проверено git show --stat fe385cbf).
  • Одно число — один источник. Диапазон не добавляет и не меняет ни одной видимой пользователю величины (роль/aria — не число; лимит 20_000 не показывается пользователю ни в каком UI) — раздел неприменим по существу, а не пропущен. test/single-source-numbers.test.mjs входит в npm test и прошёл.

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

  • Полную матрицу demo/smoke_*.mjs (212 файлов) — обоснование в разделе «Как проверялось».
  • npm run golden:verify — ни один голден-сценарий не открывает затронутый диалог; обоснование там же.
  • npm run invariants и python -m pytest tests_backend — диапазон их не задевает (геометрия/толщина/layout не тронуты; .py-файлов в диффе нет).
  • Перфоманс-профили — не названы в AC, не тронуты.
  • Реальный визуальный вид нативного диалога в живом Home Assistant (смена HA-хрома на нативную оболочку, которую ТЗ прямо называет видимым компромиссом) — только по коду и accessibility-дереву Chromium; ручного просмотра в настоящем HA в этом цикле нет и не может быть.

Вердикт

Единственная находка — Medium, в скоупе задачи (устаревший отпечаток docs/images/screenshots.json после правок в src/**), чинится тем же шагом, что автор уже применяет в релизном процессе. Все 12 AC доказаны тестом или смоком, который я лично прогнал и который умеет падать (мутанты подтверждают это для трёх ключевых контрактов). High-находок нет.

Жёлтый.