Files
houseplan-card/docs/specs/489-data-hp-contract.md
T
2026-09-08 21:04:21 +03:00

406 lines
28 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.
# #489 — Объявленный `data-hp`-контракт для UI и E2E
- **Issue:** https://github.com/Matysh/houseplan-card/issues/489
- **Тип / приоритет:** feature + tests + docs / P2
- **Трек:** полный; задача охватывает несколько UI-поверхностей и вводит новый
публичный DOM-контракт со стабильностью между релизами
- **Оценка:** пользовательская ценность 7/10; ценность для разработки 9/10;
сложность 6/10; риск 6/10
- **Связано:** #486; `docs/STYLING-HOOKS.md`,
`demo/smoke_styling_hooks.mjs`
## 1. Сценарий
Персона — Home admin или разработчик интеграционного теста, который проверяет
House Plan внутри настоящего Home Assistant. Поверхности — обычная dashboard
card, отдельная sidebar panel #486, три редактора и общие диалоги. Момент —
автоматическая проверка загрузки, навигации и основных действий перед выпуском,
либо точечная настройка card-mod опытным пользователем.
Внешний тестовый репозиторий `houseplan-e2e` не имеет права читать приватные
поля экземпляра карточки, случайные CSS-классы или текущую вложенность DOM. Ему
нужны небольшие, объявленные и версионируемые точки привязки.
## 2. Что человек увидит до и после
**До:** интерфейс выглядит и работает как обычно, но внешний тест вынужден
искать служебные элементы по нестабильным классам и не может честно определить,
готова ли карточка.
**После:** интерфейс выглядит и работает ровно так же, а тесты и card-mod могут
находить объявленные элементы и состояние через стабильные атрибуты.
## 3. Проблема
`docs/STYLING-HOOKS.md` уже обещает стабильность объектам самого плана:
устройствам, комнатам, проёмам, стенам, декору и вкладкам пространств. Но
служебные поверхности не покрыты единообразно:
- готовность читается из приватного `_booting`, а режим — из `_mode`;
- шапочные действия ищутся по `.settings-button`, `.pdf-button`, `.zb` и
другим классам;
- `hp-dialog` не сообщает назначение, а его основные действия не имеют общей
семантической точки привязки;
- тулбары редакторов, их инструменты, закрытие и вторичный трей не образуют
объявленного контракта;
- empty state, toast и app bar sidebar panel не имеют стабильных селекторов;
- нет одного машиночитаемого источника, по которому внешний репозиторий может
проверить допустимость своих селекторов;
- текущий smoke доказывает объектные styling hooks, но не новый test contract.
Одновременно в исходниках есть диагностические `data-hp` (`iso-*`,
`zigbee-topology-*`, `resize-*` и другие). Сам факт существования такого
атрибута не должен случайно превращать его в обещание совместимости.
## 4. Скоуп
1. Расширить `docs/STYLING-HOOKS.md` отдельным разделом **Test hooks** на
английском: назначение, селекторы, словари значений и политика стабильности.
2. Добавить `data-hp` на уже существующие элементы шапки, редакторов,
диалогов, empty state, toast и sidebar panel без добавления обёрток и без
изменения CSS.
3. Публиковать на каждом корневом `ha-card` готовность и текущий режим.
4. Ввести машинный инвентарь `docs/data-hp-contract.json` и unit-проверку его
согласованности с исходниками.
5. Расширить `demo/smoke_styling_hooks.mjs` доказательствами DOM-состояний,
которые нельзя подтвердить одним статическим поиском.
6. Там, где существующий smoke уже обращается к охваченному элементу по
внутреннему классу, перевести его на объявленный hook; полный перенос всех
остальных smoke не требуется.
7. Добавить пользовательскую запись в оба changelog со ссылкой на #489.
## 5. Не входит
- изменение разметки, вложенности, визуальных стилей, размеров или расположения;
- новые кнопки, диалоги, тосты, действия или пользовательские настройки;
- изменение логики готовности, загрузки, редакторов или разрешений;
- backend seed/reset API и код внешнего `houseplan-e2e`;
- обещание стабильности для CSS-классов, shadow DOM-вложенности и внутренних
диагностических `data-hp`;
- обязательный перевод всей существующей smoke-suite на новые селекторы;
- добавление русскоязычного раздела в User Guide: это технический контракт, а
не новый пользовательский сценарий.
## 6. Контракт поведения
### 6.1 Общие правила
1. `data-hp` остаётся единым атрибутом «что это за элемент».
2. Все значения из публичного инвентаря стабильны. Переименование или удаление
требует записи в оба changelog и сохранения старого значения либо
эквивалентного совместимого селектора на протяжении одной следующей
**стабильной** версии. Добавление нового значения совместимо.
3. Атрибут не создаёт поведения и не участвует в стилях продукта. Удаление
атрибута через card-mod не должно ломать работу интерфейса.
4. Когда элемент не существует по действующим permission/mode/state-правилам,
соответствующий hook также отсутствует. Контракт не требует скрытой копии
элемента.
5. Тесты выбирают элементы внутри shadow root конкретного экземпляра House
Plan. Уникальность между несколькими карточками на странице не обещается.
### 6.2 Корневое состояние
Каждый отрисованный корневой `ha-card`, включая boot/fixed-floor/empty/error
ветки, получает:
| Атрибут | Значения | Правило |
|---|---|---|
| `data-hp-state` | `booting`, `ready` | `booting`, пока действует существующий `_booting`; после его завершения — `ready` |
| `data-hp-mode` | `view`, `plan`, `devices`, `decor` | Публичное имя текущего режима; словарь совпадает с уже опубликованным `mode-devices` и существующим `data-editor-navigation="devices"` |
Empty card после завершённой загрузки является `ready`, даже если в ней нет ни
одного пространства. Ошибка fixed-floor не возвращает карточку в `booting`.
### 6.3 Публичные `data-hp` для шапки и состояния
| `data-hp` | Существующий элемент | Условие существования |
|---|---|---|
| `settings` | кнопка общих настроек | как сейчас: writable normal mode |
| `pdf` | кнопка PDF | как сейчас |
| `support` | кнопка помощи/обратной связи | как сейчас |
| `zoom-in` | кнопка `+` zoom | как сейчас: в ordinary plan render; в kiosk остаётся в DOM внутри скрытого CSS header |
| `zoom-out` | кнопка `−` zoom | как сейчас: в ordinary plan render; в kiosk остаётся в DOM внутри скрытого CSS header |
| `zoom-fit` | кнопка «вписать всё» | как сейчас: в ordinary plan render; в kiosk остаётся в DOM внутри скрытого CSS header |
| `space-add` | кнопка `+` рядом со вкладками | по существующим permission/fixed-floor/kiosk правилам |
| `space-settings` | шестерёнка внутри вкладки | `data-id` равен id пространства |
| `empty` | существующий контейнер пустого состояния | нет пространств либо отображается fixed-floor pending/error |
| `create-space` | существующая CTA первого пространства | только если CTA уже разрешена |
| `toast` | существующий toast | дополнительно `data-kind="message"`; семантика severity в #489 не вводится |
`space-settings` несёт `data-id`, потому что на экране может быть несколько
шестерёнок. Остальные шапочные actions в пределах одной карточки единичны.
### 6.4 Редакторы
| `data-hp` | Дополнительный атрибут | Контракт |
|---|---|---|
| `toolbar` | `data-kind="plan|device|decor"` | корень видимого primary toolbar |
| `tool` | `data-tool="<stable-id>"` | существующая кнопка выбора инструмента/команды редактора |
| `editor-close` | — | существующая кнопка X в каждом toolbar и X активной mode-tab |
| `tray` | `data-kind="<group-id>"` при наличии группы | корень видимой secondary/context surface редактора |
Минимальный словарь `data-tool`:
- Plan: `select`, `draw`, `column`, `merge`, `split`, `resize`,
`wall-thickness`, `delete-room` и id существующих групповых launcher-кнопок;
- Device: `add-device`, `device-inbox`, `icon-rules` и id существующих
групповых launcher-кнопок;
- Decor: `select`, `backdrop`, `line`, `rect`, `ellipse`, `text`, `furniture`,
`image`, `erase` и id существующих групповых launcher-кнопок.
Undo/Redo, picker и отдельные save/cancel внутри трея не объявляются `tool`:
они не выбирают инструмент. Существующий `data-editor-navigation` сохраняется;
новый `editor-close` дополняет его, а не заменяет в переходный период.
### 6.5 Диалоги
Сам host каждого `hp-dialog` всегда получает `data-hp="dialog"`. Каждый call
site задаёт один стабильный broad kind из закрытого стартового словаря:
`space`, `room`, `marker`, `opening`, `physical`, `decor`, `settings`, `pdf`,
`support`, `confirm`, `onboarding`, `import`, `backup`, `kiosk`, `rules`,
`device-inbox`, `vacuum`, `summary`, `info`.
Новые kind добавляются совместимо. Broad kind описывает пользовательскую
задачу, а не имя приватного метода; несколько вариантов одного workflow могут
иметь одинаковый kind.
На существующих главных действиях диалога:
- `data-hp="dialog-confirm"` — Save/Create/Apply/Send/Import/Run и другое
действие, которое подтверждает результат данного диалога;
- `data-hp="dialog-cancel"` — Cancel/Close/Back, которое закрывает диалог без
принятия результата;
- destructive secondary actions вроде Delete не получают `dialog-confirm`,
если открывают отдельный confirm workflow;
- встроенная fallback-кнопка X `hp-dialog` получает `dialog-cancel`; внутреннюю
close-кнопку HA shadow tree контракт не охватывает.
У многошагового диалога hooks относятся к текущему шагу. Если у шага нет
явного confirm/cancel button, синтетическая кнопка не добавляется.
### 6.6 Sidebar panel
В shadow root `<houseplan-panel>` существующие элементы получают:
- `data-hp="panel-menu"` на кнопке, отправляющей `hass-toggle-menu`;
- `data-hp="panel-title"` на видимом заголовке House Plan.
Это не распространяет styling contract карточки через два shadow root и не
обещает стабильность остальной panel-shell разметки.
## 7. UX
Визуальная и интерактивная дельта равна нулю:
- подписи, доступность, tab order и focus management не меняются;
- кнопки остаются видимыми и активными ровно в прежних условиях;
- режимы и readiness лишь проецируются в DOM, а не вычисляются заново;
- никаких тестовых индикаторов, debug-панелей или пользовательских настроек не
появляется.
Для card-mod действует прежнее предупреждение: поддерживаются только явно
объявленные hooks, но не произвольный CSS и не внутренняя структура.
## 8. Модель данных и миграция
Пользовательская конфигурация и backend storage не меняются; миграции нет.
`docs/data-hp-contract.json` имеет версионированную схему:
```json
{
"schemaVersion": 1,
"attribute": "data-hp",
"hooks": {
"device": { "elements": ["div"], "since": "1.59.0-beta.3", "audience": ["styling", "test"] },
"settings": { "elements": ["button"], "since": "1.73.0-beta.7", "audience": ["test"] }
},
"rootAttributes": {
"data-hp-state": { "element": "ha-card", "values": ["booting", "ready"], "since": "1.73.0-beta.7" }
},
"internalPrefixes": ["iso-", "zigbee-topology-", "resize-", "plan-snap-", "hidden-wall-"]
}
```
Полный файл содержит все уже объявленные значения §3
`STYLING-HOOKS.md`, новые значения #489, `data-hp-mode`, связанные словари
`data-kind`/`data-tool` там, где они являются частью нового селектора, и
исчерпывающие исключения для существующих внутренних exact values/prefixes.
`since` существующих hooks восстанавливается по документации/истории, новых —
первая планируемая версия выпуска текущей линии (`1.73.0-beta.7`). Если до
релиза линия изменится, release commit обновляет только metadata `since`.
JSON не импортируется runtime-кодом и не попадает в initial bundle. Это
документ и вход для тестов/внешнего E2E.
## 9. i18n
Новых пользовательских строк нет. EN/RU/DE/FR bundles не меняются. Раздел Test
hooks пишется на английском как разработческий контракт; существующая русская
User Guide лишь сохраняет ссылку на styling hooks, если такая ссылка уже есть.
## 10. Критерии приёмки
### AC1 — корневые состояние и режим
Каждый render branch `ha-card` несёт допустимые `data-hp-state` и
`data-hp-mode`; переход View → каждый editor → View обновляет mode без чтения
приватных полей.
**Доказательство:** unit/source-contract test + расширенный
`node demo/smoke_styling_hooks.mjs`.
### AC2 — шапка, empty и toast
Все доступные в текущем состоянии header actions находятся по селекторам из
§6.3; writable empty state имеет `empty` и `create-space`, read-only empty не
получает ложной CTA; toast находится как `toast[data-kind="message"]`.
**Доказательство:** `demo/smoke_styling_hooks.mjs` на обычной, empty writable
и empty read-only fixtures.
### AC3 — три редактора
В каждом editor существует один видимый `toolbar` с правильным `data-kind`,
каждая заявленная tool button имеет стабильный `data-tool`, закрытие находится
как `editor-close`, а открытая secondary surface — как `tray`.
**Доказательство:** browser smoke проходит Plan/Device/Decor и открывает по
одному group/tray; unit test сверяет словари inventory с source call sites.
### AC4 — диалоги
Каждый `hp-dialog` host имеет `dialog` и допустимый `data-kind`; все
существующие семантические confirm/cancel buttons размечены по §6.5, не меняя
действия и focus flow.
**Доказательство:** source-contract test запрещает call site без kind;
unit-тест `hp-dialog` проверяет host/fallback X; smoke открывает settings,
space/onboarding и confirm workflows и выполняет действия через hooks.
### AC5 — sidebar panel
Menu и title существующей #486 panel находятся как `panel-menu` и
`panel-title`, при этом menu по-прежнему отправляет ровно одно
`hass-toggle-menu`.
**Доказательство:** unit test `houseplan-panel`.
### AC6 — машинный инвентарь закрыт и сам себя защищает
`docs/data-hp-contract.json` валиден и содержит каждый публичный hook. Новый
`test/data-hp-contract.test.mjs` падает, если:
- объявленное значение больше не встречается в исходниках;
- в исходниках появилось статическое `data-hp`, которого нет в публичном
списке и которое не разрешено внутренним prefix/exact allowlist;
- `hp-dialog` call site не имеет kind;
- обязательное поле `elements`, `since` или `audience` отсутствует;
- переименованный fixture-hook не совпадает с inventory.
**Доказательство:** `npm test`, включая локальный mutant/fixture case внутри
самого unit-теста.
### AC7 — документация и совместимость
`STYLING-HOOKS.md` отличает styling и test audiences, перечисляет селекторы и
политику переходного периода. Оба changelog содержат понятную запись со ссылкой
на #489. Пользовательская config и видимый DOM/layout не меняются кроме новых
атрибутов.
**Доказательство:** review diff + обязательный
`node scripts/check-docs.mjs` после принятия канонического screenshot-артефакта
и обычный build.
### AC8 — обязательные гейты
Проходят `npm run typecheck`, `npm test`, `npm run build`,
`npm run bundle:sync`, `npm run bundle:budget` и локальный
`node demo/smoke_styling_hooks.mjs`. Поскольку меняется `src/**`, дополнительно
обязателен `node scripts/check-docs.mjs`: до пересъёмки он должен честно
сообщить об устаревшем source fingerprint, после принятия нового комплекта —
пройти зелёным.
**Доказательство:** точные команды и результаты в комментарии #489 перед
`S7-code-review`.
## 11. План автотестов
1. Новый Node test читает JSON, валидирует схему и сканирует `src/**/*.ts`,
включая статические и ограниченный набор динамических Lit assignments.
2. Тест содержит контролируемую строку-мутант: замена одного публичного
значения обязана дать ошибку `undeclared/missing`, чтобы доказать, что gate
не является формальным чтением JSON.
3. Unit `hp-dialog` проверяет автоматический host hook и fallback close hook;
существующие focus/Escape тесты остаются зелёными.
4. Unit panel-shell проверяет два hooks и отсутствие дублированного menu event.
5. Styling smoke последовательно проверяет ready/view, header, toast, empty,
три editor toolbars, tray и репрезентативные dialog kinds/actions.
6. Полный smoke/golden/performance остаются предрелизными гейтами; golden
baseline меняться не должен, поскольку пиксельной дельты нет.
## 12. Риски и меры
- **Пропущенный conditional branch.** Root attributes выносятся в один
маленький resolver/helper и используются во всех ранних `ha-card` returns;
smoke отдельно проходит empty и обычный path.
- **Неполная классификация диалогов.** Unit сканирует все `<hp-dialog ...>` и
запрещает неразмеченный call site; broad kinds не зависят от имени метода.
- **Ложное превращение diagnostics в API.** Публичные hooks и внутренний
allowlist лежат раздельно; docs прямо отрицает стабильность allowlist.
- **Regex gate пропускает динамику.** Динамические assignments допускаются
только через явно проверенные expressions/fixtures; runtime smoke доказывает
результирующий DOM.
- **Сломанный внешний E2E при расширении.** `schemaVersion` меняется только при
несовместимом изменении структуры JSON; добавление hook не меняет версию
схемы.
- **Рост initial bundle.** JSON не импортируется runtime-кодом; атрибуты дают
только небольшую строковую дельту, проверяемую bundle budget.
## 13. Откат
До выпуска ветка откатывается обычным revert целиком. После выпуска удалять
или переименовывать опубликованные hooks немедленно нельзя: сначала changelog и
одна стабильная версия совместимого перехода. Runtime-атрибуты можно перестать
использовать внутри продукта в любой момент, пока публичные селекторы остаются
доступны. JSON schema v1 сохраняется, либо при несовместимой структуре получает
новую `schemaVersion`.
Пользовательские данные откатывать не требуется.
## 14. Release-артефакты
- source изменения в `src/**` и тесты в `test/**`, `demo/**`;
- `docs/data-hp-contract.json` и обновлённый `docs/STYLING-HOOKS.md`;
- запись `docs/specs/README.md` и двусторонняя ссылка issue ↔ ТЗ;
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же user-visible commit;
- синхронные bundle trees после `npm run bundle:sync`;
- канонический комплект docs screenshots из workflow **Docs screenshots** на
точном implementation SHA, принятый командой
`npm run docs:accept -- --reviewed --from=<распакованный-артефакт>`; локальная
самостоятельная пересъёмка PNG не принимается;
- без golden baseline, если visual diff действительно нулевой.
## 15. Принятые предположения
Эти технические решения приняты предположительно и могут быть свободно
изменены ревьюером без блокировки владельца:
- JSON использует object map `hooks[value]`, чтобы внешний E2E мог проверять
селектор без линейного поиска;
- `audience` различает `styling` и `test`, но оба набора получают одинаковое
обещание стабильности;
- toast в этой задаче имеет один честный kind `message`; severity нельзя
достоверно восстановить из переведённого текста и она не нужна заявленному
E2E-сценарию;
- dialog kinds намеренно broad и не кодируют каждый внутренний подшаг;
- `devices` сохраняет уже опубликованное имя `.stage.mode-devices` и значение
`data-editor-navigation="devices"`; отдельное третье имя `device` для того же
режима не вводится;
- следующая запись `since` предварительно равна `1.73.0-beta.7` и уточняется
release commit, если версия линии изменится;
- внутренние исключения могут быть exact values сверх перечисленных в issue,
если они уже существуют на исходной вершине `dev`; это не делает их API;
- открытых продуктовых вопросов нет.