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

210 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` пуст).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `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
```