21 KiB
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.mjswarning про запас < 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(«прямое совпадение» + «слабая связь») сверх названного в ACsmoke_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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
e2d351cf6dfaa46c478d4018f20b580da3a974c8git log --all --format='%H %T' | grep e2d351cf6dfa - ТЗ
docs/specs/489-data-hp-contract.md, блоб127ce9505c272a9a4152b92157d71d78b96b1070git log --all --find-object=127ce9505c272a9a4152b92157d71d78b96b1070 -- docs/specs/489-data-hp-contract.md