docs: review document for #489

Issue: #489
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-08 19:35:20 +00:00
parent 5586e0591c
commit ea6061e9fa
+209
View File
@@ -0,0 +1,209 @@
# 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
```