Files
houseplan-card/docs/reviews/CODE-REVIEW-489-r1.md
T
2026-09-08 19:35:20 +00:00

21 KiB
Raw Blame History

CODE-REVIEW-489-r1

Issue: #489 — Объявленный data-hp-контракт для UI и E2E Ветка: issue/489-data-hp-contract Материал: git diff origin/dev...HEAD, git log --oneline origin/dev..HEAD на SHA 5586e0591cab96a9fb59e6a1eda17e33f4ea296c (сверено git rev-parse HEAD непосредственно перед выводом — совпадает) Заход: r1 (код-ревью первый раз для этого issue; ТЗ прошло два захода ревью, оба зафиксированы отдельными документами SPEC-REVIEW-489-r1/r2, зелёный вердикт получен на r2) Вердикт: зелёный · High: 0 · Medium: 0


1. Скоуп проверки

Диапазон коммитов origin/dev..HEAD (7 коммитов, все с трейлером Issue: #489):

9cf10bdd docs: specify public data-hp test contract (#489)         User-Visible: no
a48827ec docs: review document for #489                            User-Visible: no
e072b222 docs: address first spec review for data-hp contract      User-Visible: no
1d2ed1e3 docs: review document for #489                            User-Visible: no
9c61ac67 feat: declare stable data-hp test contract (#489)         User-Visible: yes
15e0efe3 docs: accept data-hp contract screenshots (#489)          User-Visible: no
5586e059 perf: recenter initial bundle ceiling for data-hp contract (#489)  User-Visible: no

User-Visible: yes стоит ровно на коммите, добавляющем поведение (новые data-hp в DOM); оба changelog правятся в нём же (docs/CHANGELOG.md, docs/CHANGELOG.ru.md) — трейлер корректен.

Изменённые файлы класса A: src/houseplan-card.ts, src/houseplan-editor-runtime.ts, src/houseplan-onboarding-runtime.ts, src/houseplan-panel.ts, src/hp-dialog.ts, src/hp-confirm.ts, src/editor-secondary.ts, src/backdrop-pick.ts, src/decor-image-editor.ts, src/space-copy-runtime.ts, src/summary-panel-editor.ts, src/pdf/hp-pdf-dialog.ts — 12 файлов. Класс B: test/data-hp-contract.test.mjs (новый), test/houseplan-panel.test.mjs, demo/smoke_styling_hooks.mjs, scripts/bundle-budget.mjs. Класс C: docs/STYLING-HOOKS.md, docs/CHANGELOG*.md, docs/data-hp-contract.json (новый, не runtime-код — данные), docs/specs/README.md, docs/images/*. Класс D: dist/**, custom_components/houseplan/frontend/**, demo/srv/assets/**.

Первый факт проверки диффа: весь код класса A — построчно только добавление атрибутов. Прогнал по всем 12 файлам фильтр добавленных строк, исключив паттерны data-hp|data-kind|data-tool|data-id=|launcherId|dataset.hp|setAttribute('data-hp' и переносы строк без содержимого. Остаток — исключительно строки, физически перенесённые из-за возросшей длины строки (@click=..., .title=..., ?disabled=... без единого символа изменений внутри), плюс один поясняющий комментарий и одно новое опциональное поле launcherId в уже существующем типе модели (используется только для вычисления data-kind тrея; ветка kind === 'palette', читающая то же поле раньше, не задета — она проверяет другое значение kind). Это прямое подтверждение §7 ТЗ («визуальная и интерактивная дельта равна нулю») чтением, а не со слов автора, и оно же — причина, по которой пункт «браузерные смоки» ниже решён так, как решён.

2. Как проверялось — таблица гейтов

Гейт Команда Результат Источник
typecheck/test/build (сверка бандла) npx tsc --noEmit, npm test, npm run build зелёный Validate CI на точном SHA 5586e059: https://github.com/Matysh/houseplan-card/actions/runs/34265047142 (success) — переиспользован, не перегонялся
Свежесть бандла (перепроверка) npm run bundle:sync build прошёл, git status после — пусто (байт-в-байт совпадение с закоммиченным деревом) прогнал сам
no-new-any node scripts/no-new-any.mjs --base origin/dev --head HEAD «Новых any нет» (167 добавленных строк в 12 файлах) прогнал сам
check-docs (обязателен, диф трогает src/**) node scripts/check-docs.mjs «Documentation checks passed (7 files, 12 external links)» прогнал сам
bundle:budget npm run bundle:budget initial View 291681 B ⩽ потолок 292500 B; общий бюджет 301066 B не тронут; предупреждение о запасе — унаследованный долг #367, не новый прогнал сам, цифры совпадают с хендоффом
test/data-hp-contract.test.mjs (новый, защищает AC6) node --test test/data-hp-contract.test.mjs 6/6 зелёных прогнал сам
test/houseplan-panel.test.mjs (AC5) node --test test/houseplan-panel.test.mjs 5/5 зелёных прогнал сам
demo/smoke_styling_hooks.mjs (назван в AC1–AC4) node demo/smoke_styling_hooks.mjs все проверки true, вывод OK прогнал сам после bundle:sync
Выборка смоков по диффу node scripts/smoke-select.mjs --base origin/dev --head HEAD 114 «прямое совпадение» + 39 «слабая связь» из 232 прогнал сам, разбор — ниже
python -m pytest tests_backend -q — не прогонял диф не трогает custom_components/**/*.py
npm run model-invariants — не прогонял диф не трогает геометрию/layout/marker.space/open_spans/записи толщины
golden:verify — не прогонял нулевой визуальный дифф заявлен в ТЗ §11.6 и подтверждён отсутствием правок в demo/golden/baselines/**; сами data-hp-атрибуты не участвуют в рендере пикселей
performance-профили — не прогонял не названы в AC; диф не касается src/iso-*, src/live-*, src/render-*, lifecycle-файлов

Разбор выборки смоков (114 прямых + 39 слабых)

Инструмент называет почти весь смок-набор, потому что новые атрибуты дописываются на тех же строках, что и существующие @click=${…this._openMarkerDialog…}, _mode, _toast и т.п. — это совпадение по символу на строке, а не по изменению поведения этого символа. Пункт 1 этого документа показывает построчно, что ни одна из этих строк не меняет ничего, кроме появления атрибута. Раз обработчик и логика не тронуты нигде в диффе, вероятность, что именно этот прогон поймает регрессию, не отличается от вероятности на случайно выбранном смоке вне выборки — реального риска, который стоило бы гасить прогоном полусотни смоков, выборка не вскрывает, она вскрывает шум построчного диффа. Решение: не гоняю ни один из 153 отмеченных смоков сверх названного в AC smoke_styling_hooks.mjs, с записью здесь как осознанного выбора, а не молчаливого пропуска. Если бы хоть один добавленный атрибут менял управляющую логику (условие рендера, обработчик, значение поля), разбор был бы другим — но такого места диф не содержит (проверено p.1 и дополнительно тремя целевыми мутациями ниже, все они бьют по новым тестам, а не по существующим смокам).

3. AC — доказательство и «чем краснеет»

AC Доказано Чем Чем краснеет (защитные AC)
AC1 корень: state/mode да test/data-hp-contract.test.mjs тест «root readiness and mode…» (считает 4 ветки <ha-card>, требует по одному data-hp-state=/data-hp-mode= на каждую) + смок (rootPublishesReadyView, mode_*_isPublished, returnToViewIsPublished) Живая мутация: убрал data-hp-state из одной из четырёх веток houseplan-card.ts → тест #489 root readiness and mode attributes cover every full-card ha-card branch красный (Expected values to be strictly equal). Дерево восстановлено, git status пуст
AC2 шапка/empty/toast да смок: headerActionsHaveStableHooks, spaceActionsHaveStableHooks, toastHasStableKind, writableEmptyIsReady/writableEmptyHasCreateAction, readOnlyEmptyIsReady/readOnlyEmptyHasNoCreateAction — все true на прогоне смок использует checkAll, которая проваливает прогон на любом false (demo/serve.mjs:75); не мутировал отдельно, но механизм проверен на другом ключе (см. AC6)
AC3 три редактора да смок: toolbar_{plan,devices,decor}_isPublished, tools_{plan,devices,decor}_arePublished, planGroupOpensPublishedTray — все true; плюс чтением сверил, что Undo/Redo и пикеры не получили data-tool (§6.4 ТЗ) не мутировал отдельно; логика идентична AC2 (та же checkAll)
AC4 диалоги да test/data-hp-contract.test.mjs тест «every hp-dialog call site declares a documented broad kind» (32 совпадения <hp-dialog, все с data-kind из закрытого словаря) + смок открывает settings-диалог и проверяет host/confirm/cancel Живая мутация: убрал data-kind="settings" с одного вызова <hp-dialog> → тест красный (hp-dialog without data-kind). Восстановлено
AC5 sidebar panel да test/houseplan-panel.test.mjs: menu.dataset.hp = 'panel-menu', title.dataset.hp = 'panel-title' — присутствуют, тест зелёный (прогнал: 5/5) проверено чтением: houseplan-panel.ts не содержит ветки без установки dataset.hp на этих двух узлах — узлы создаются один раз в connectedCallback
AC6 инвентарь + самозащита да test/data-hp-contract.test.mjs целиком (6/6): схема, полнота (sourceValues ⇄ publicValues), встроенный мутант «переименованный хук», проверка каждого hp-dialog, root-веток Живая мутация 1: переименовал data-hp="settings" → settings-x в исходнике → тест «every source data-hp value…» красный (undeclared data-hp values: settings-x), и сам встроенный мутант-тест тоже упал (регэксп ждал другую строку) — оба сигнала сработали. Живая мутация 2 и 3 — см. AC1/AC4
AC7 документация/совместимость да docs/STYLING-HOOKS.md §7 (7.1–7.7) описывает audience, политику переходного периода, ссылку на JSON; оба changelog правлены в коммите 9c61ac67 (тот же, где новое поведение); node scripts/check-docs.mjs — зелёный (прогнал) не защитный AC (текст/документация) — свидетель обычное сравнение, не мутант
AC8 обязательные гейты да таблица §2 этого документа —

4. Находки

Нет. High: 0, Medium: 0, Low: 0.

Проверенные потенциально спорные места оказались осознанными решениями, задокументированными в ТЗ и подтверждёнными кодом:

  • data-hp="toolbar" использует data-kind="device" (единственное число), а data-hp-mode — "devices" (множественное). Не рассогласование: ТЗ §6.4 прямо фиксирует device для toolbar, отдельно от корневого режима, и это осознанно пережило r1→r2 спек-ревью (M3 в r1 касался только data-hp-mode, а не data-kind тулбара). Смок явно транслирует это соответствие (mode === 'devices' ? 'device' : mode).
  • Диалог калибровки вакуума и диалог удаления комнаты несут по два data-hp="dialog-confirm" (Fit/Apply, Keep-walls/Delete-with-walls) — прямо разрешено ТЗ §6.5 («Alternate accepted outcomes may produce more than one confirm button»).
  • Кнопки Skip (импорт floors) и Reset (kiosk-масштаб, живёт на слайдерах) не получили ни dialog-confirm, ни dialog-cancel — они не Save и не Close/Cancel/Back, ТЗ размечает только «принципиальные» действия диалога; это не пропуск, а точное соответствие §6.5.
  • Кнопка .tabadd/space-add в kiosk не рендерится вовсе (существующее условие !this._kiosk), а zoom-кнопки в kiosk остаются в DOM под display:none — оба варианта описаны в ТЗ §6.3/§7.3 STYLING-HOOKS.md дословно, ровно то, что было закрыто как M1 в r1 спек-ревью.

Одно число — один источник: диф не вводит ни одной новой видимой пользователю величины — только DOM-атрибуты, не участвующие в рендере и не видимые вне DevTools/E2E. Вопрос неприменим к этой задаче.

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

  • Все 4 корневых <ha-card>-ветки (pending, invalid, !model.length, обычный рендер) несут data-hp-state/data-hp-mode, значения совпадают с типом _mode: 'view'|'plan'|'devices'|'decor' — прочитано и подтверждено тестом.
  • Все 33 вызова <hp-dialog> (32 в src/*.ts, 1 в src/pdf/hp-pdf-dialog.ts) несут data-kind из объявленного словаря docs/data-hp-contract.json (19 значений, совпадает с ТЗ §6.5 построчно).
  • data-hp="dialog-confirm"/dialog-cancel" расставлены по каждому найденному главному действию диалогов — прочитано построчно по всем диффам класса A, расхождений с §6.5 не найдено.
  • Host <hp-dialog> получает data-hp="dialog" через setAttribute в connectedCallback (вне HA/native ветки рендера) — верно для всех реализаций диалога, включая нативный HA fallback.
  • docs/data-hp-contract.json: словарь data-tool покрывает весь список ТЗ §6.4 для Plan/Device/Decor, internalPrefixes/internalExactValues совпадают с §7.7 STYLING-HOOKS.md, retiredHooks.room-draft соответствует #478.
  • Панель (#486): panel-menu/panel-title расставлены один раз при создании узлов, тест houseplan-panel.test.mjs обновлён и зелёный.
  • Оба changelog содержат запись со ссылкой на #489 в том же коммите, где появляется новое поведение (9c61ac67, User-Visible: yes).
  • docs/specs/README.md содержит двустороннюю ссылку issue ↔ ТЗ.
  • Bundle ceiling пересчитан (292000 → 292500) с обоснованием в комментарии кода и не меняет общий бюджет — соответствует ТЗ §12 «Рост initial bundle».
  • scripts/bundle-budget.mjs warning про запас < 15000 Б — унаследованный долг #367, явно так и оформлен, не находка этой задачи.

6. Чего не проверял и почему

  • python -m pytest tests_backend — диф не трогает custom_components/**/*.py.
  • npm run model-invariants / scripts/model-invariants.mjs — диф не трогает геометрию, layout, marker.space, open_spans или записи толщины стен; вся правка — DOM-атрибуты разметки.
  • npm run golden:verify / golden:capture — ТЗ §11.6 заявляет нулевой визуальный дифф, demo/golden/baselines/** не менялся в диффе, а сами проверенные атрибуты не участвуют ни в одном стиле — расхождение было бы видно по изменившимся файлам baseline, которых нет.
  • performance-профили (large-house-isometric-v1, large-house-interaction-v1) — не названы в AC, диф не касается src/iso-*, src/live-*, src/render-*, houseplan-render-lifecycle.ts, houseplan-card.ts-lifecycle-путей (только разметочные правки внутри уже существующих веток houseplan-card.ts, не по этим путям).
  • 153 смока, отмеченных smoke-select.mjs («прямое совпадение» + «слабая связь») сверх названного в AC smoke_styling_hooks.mjs — решение и обоснование в §2 «Разбор выборки смоков».
  • Полный smoke/golden/performance_smoke матрица — предрелизный гейт (PROCESS.md §8), не гейт код-ревью; согласно issue-переписке, полный CI со всеми mutation-shards на этом SHA уже прогнан автором (https://github.com/Matysh/houseplan-card/actions/runs/34265047142) и документационный скриншот-workflow отдельно (https://github.com/Matysh/houseplan-card/actions/runs/34263093186) — не перепроверял оба прогона вручную сверх сверки самого SHA и повторного локального check-docs.

7. Материал раунда

  • SHA материала: 5586e0591cab96a9fb59e6a1eda17e33f4ea296c (= git rev-parse HEAD на момент вывода вердикта).
  • Диапазон: origin/dev..HEAD, 7 коммитов, диф git diff origin/dev...HEAD — 76 файлов (полный список в git diff --stat, приведён частично в §1).
  • Рабочая копия после всех проверок и трёх пробных мутаций — чистая (git status --porcelain пуст, git diff --stat пуст).

Материал раунда

  • Ветка: issue/489-data-hp-contract, коммит 5586e0591cab — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: e2d351cf6dfaa46c478d4018f20b580da3a974c8
    git log --all --format='%H %T' | grep e2d351cf6dfa
    
  • ТЗ docs/specs/489-data-hp-contract.md, блоб 127ce9505c272a9a4152b92157d71d78b96b1070
    git log --all --find-object=127ce9505c272a9a4152b92157d71d78b96b1070 -- docs/specs/489-data-hp-contract.md