mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
323 lines
23 KiB
Markdown
323 lines
23 KiB
Markdown
# ТЗ #443 — Полиш маршрутов карт робота
|
||
|
||
- Issue: https://github.com/Matysh/houseplan-card/issues/443
|
||
- Приоритет: P3, `bug` / `polish` / `vacuum`
|
||
- Статус ТЗ: готово к ревью
|
||
- Маршрут: full; меняются frontend и backend semantics, single-space export,
|
||
локализованный Device editor и performance-sensitive render snapshot
|
||
- Связанные контракты: #162 (маршрутизация карт по пространствам), #441
|
||
(атомарное добавление новой карты робота)
|
||
|
||
## Сценарий
|
||
|
||
Home admin удаляет у робота последний явный маршрут карты, редактирует старую
|
||
конфигурацию с маршрутами в удалённые пространства либо показывает большой
|
||
многоэтажный план с сотнями устройств. Пустой список маршрутов должен означать
|
||
осознанное отсутствие маршрутов, строки с потерянным пространством должны быть
|
||
понятно собраны, а View не должен повторно просматривать все не относящиеся к
|
||
роботам устройства ради vacuum overlay.
|
||
|
||
## Что человек увидит до и после
|
||
|
||
**До:** `map_routes: []` может снова включить оставшийся legacy `calibration` и
|
||
показать робота/карту там, откуда пользователь удалил маршрут. При экспорте
|
||
одного пространства отфильтрованный до пустоты список может превратиться в
|
||
`null` и создать тот же эффект после импорта. Маршруты в удалённые пространства
|
||
помечены построчно и оказываются в конце, но визуально не образуют явную группу.
|
||
Каждый render vacuum layer ещё раз перебирает все устройства плана.
|
||
|
||
**После:** любой массив `map_routes`, включая пустой, является явной authority;
|
||
legacy fallback действует только при отсутствующем или `null` поле. Экспорт не
|
||
теряет осознанную пустоту. Потерянные маршруты собраны в локализованную группу
|
||
«Пространство удалено», остаются редактируемыми и имеют стабильный порядок.
|
||
Vacuum renderer получает сохранённый в snapshot список только роботов, сохраняя
|
||
межэтажные маршруты и визуальное поведение.
|
||
|
||
## Подтверждённая проблема
|
||
|
||
1. `src/vacuum-routes.ts::effectiveRoutes()` и
|
||
`custom_components/houseplan/vacuum_routes.py::effective_routes()` выбирают
|
||
explicit routes только при непустом массиве/списке. Это противоречит #162
|
||
§7.3 и `docs/VACUUM.md`: наличие `map_routes` как массива уже означает
|
||
единственную route authority.
|
||
2. Single-space export в `custom_components/houseplan/import_export.py`
|
||
записывает `kept_routes or None`. Когда все маршруты отфильтрованы, явная
|
||
пустота теряется, а оставшийся в marker legacy `calibration` может ожить при
|
||
последующем чтении.
|
||
3. Заявленная в issue случайная перестановка missing-space строк по коду не
|
||
подтверждается: comparator уже детерминирован по space order, `map_id` и
|
||
source, а schema запрещает повтор пары source/map. Подтверждён UX-недочёт:
|
||
строки лишь неявно идут в конце и не имеют отдельной группы.
|
||
4. `_captureRenderDeviceSnapshot()` уже обходит все устройства и рассчитывает
|
||
vacuum facts. Затем `_renderVacuums(this._renderDevices, ...)` повторно
|
||
перебирает весь device roster, хотя vacuum renderer нужны только роботы.
|
||
|
||
## Скоуп
|
||
|
||
В скоупе:
|
||
|
||
- единая frontend/backend семантика absent, `null`, пустого и непустого
|
||
`map_routes`;
|
||
- сохранение явного пустого массива при single-space export;
|
||
- явная локализованная группа строк, чьё `space_id` больше не существует;
|
||
- стабильный порядок валидных и потерянных маршрутов;
|
||
- vacuum-only subset в immutable render snapshot и его использование слоем
|
||
vacuum overlay;
|
||
- сохранение межэтажного отображения робота, hidden/static правил и continuity
|
||
при временном отсутствии нового snapshot;
|
||
- unit, backend, browser smoke, mutation и performance evidence;
|
||
- актуализация документации и обоих changelog.
|
||
|
||
## Не-скоуп
|
||
|
||
- новый каталог vacuum integrations, обнаружение карт или изменение протокола
|
||
источников;
|
||
- изменение route identity, калибровки, puck/trail/room outline либо команд
|
||
уборки;
|
||
- автоматический выбор нового пространства для потерянного маршрута;
|
||
- автоматическое удаление missing-space route;
|
||
- миграция или eager rewrite сохранённых legacy markers;
|
||
- изменение семантики malformed non-array `map_routes`;
|
||
- оптимизация остальных проходов по device roster и полный рефактор render
|
||
snapshot;
|
||
- изменение удаления пространства за пределами экспортного edge case;
|
||
- изменение static space-card UX или редакторов вне vacuum maps section.
|
||
|
||
## Контракт поведения
|
||
|
||
### 1. Authority `map_routes`
|
||
|
||
Frontend и backend используют одну таблицу решений:
|
||
|
||
| Состояние marker | Эффективные маршруты |
|
||
|---|---|
|
||
| поле `map_routes` отсутствует | legacy fallback из `calibration`, как сейчас |
|
||
| `map_routes: null` | legacy fallback из `calibration`, как сейчас |
|
||
| `map_routes: []` | пустой explicit result; legacy полностью игнорируется |
|
||
| `map_routes: [ ... ]` | только нормализованные explicit routes; legacy игнорируется |
|
||
|
||
Проверка должна зависеть от типа массива/списка, а не от его truthiness или
|
||
длины. Пустой массив не создаёт overlay, trail, puck или route-space
|
||
presentation из legacy calibration.
|
||
|
||
Malformed non-array значения остаются на прежнем compatibility path; #443 не
|
||
расширяет schema и не пытается молча исправлять неизвестные будущие формы.
|
||
Никакой фоновой записи конфигурации при чтении не выполняется.
|
||
|
||
### 2. Single-space export
|
||
|
||
Если исходный vacuum marker содержит массив `map_routes`, export одного
|
||
пространства сохраняет `map_routes` массивом даже тогда, когда фильтрация не
|
||
оставила ни одного route. Результат в этом случае — `[]`, а не `null`,
|
||
отсутствующее поле или возврат legacy calibration.
|
||
|
||
Если исходное поле отсутствовало или было `null`, действующее legacy-compatible
|
||
поведение не меняется. Экспорт не создаёт synthetic routes и не удаляет
|
||
legacy-поля из source marker: безопасность обеспечивается сохранённой explicit
|
||
empty authority.
|
||
|
||
### 3. Группировка потерянных пространств
|
||
|
||
Device editor показывает маршруты в следующем порядке:
|
||
|
||
1. маршруты существующих пространств — по текущему порядку spaces, затем по
|
||
`map_id`, source и route id как детерминированному tie-breaker;
|
||
2. отдельная группа потерянных маршрутов после всех валидных групп — по
|
||
`map_id`, source и route id.
|
||
|
||
Группа имеет видимый локализованный заголовок с семантикой «Пространство
|
||
удалено». Рекомендуемые строки:
|
||
|
||
- ru: «Пространство удалено»;
|
||
- en: “Deleted space”;
|
||
- de: “Gelöschter Bereich”;
|
||
- fr: “Espace supprimé”.
|
||
|
||
Существующая построчная индикация missing-space и общий warning сохраняются:
|
||
заголовок помогает сканировать список, но не является единственным объяснением
|
||
ошибки. Каждая строка по-прежнему доступна для выбора, редактирования и
|
||
удаления; House Plan не угадывает замену пространства. Pending draft добавления
|
||
маршрута не смешивается с группой и сохраняет контракт #441.
|
||
|
||
Если потерянных маршрутов нет, заголовок и дополнительная группа не
|
||
рендерятся. Порядок валидных строк визуально не меняется.
|
||
|
||
### 4. Vacuum-only render snapshot
|
||
|
||
Capture одного device snapshot формирует immutable vacuum-only subset из той
|
||
же согласованной копии device roster, на которой рассчитаны facts. Vacuum
|
||
renderer в обычном View перебирает этот subset, а не полный список устройств.
|
||
|
||
Subset обязан содержать всех роботов плана, а не только устройства текущего
|
||
пространства. Поэтому сохраняются:
|
||
|
||
- показ робота на route-space другого этажа относительно пространства базы;
|
||
- выбор map route и route facts из того же snapshot;
|
||
- suppression скрытых и static devices;
|
||
- отсутствие повторного resolve route authority внутри renderer;
|
||
- snapshot continuity: незавершённый новый capture не смешивает старые devices
|
||
с новыми facts.
|
||
|
||
Fallback полного списка допустим только на существующем snapshot-less
|
||
инициализационном/тестовом пути и не должен становиться обычным render path.
|
||
Изменение сокращает повторный render scan с `O(N)` до `O(V)`, где `N` — все
|
||
device markers, а `V` — vacuum markers; capture остаётся `O(N)` и не обязан
|
||
ускоряться в этой задаче.
|
||
|
||
## Данные и совместимость
|
||
|
||
- Schema version и persisted shape не меняются; миграции нет.
|
||
- Старые конфигурации без `map_routes` и с `map_routes: null` продолжают читать
|
||
legacy `calibration`.
|
||
- Уже сохранённый `map_routes: []` меняет только ошибочную read semantics и
|
||
перестаёт оживлять legacy routes.
|
||
- Frontend и backend обязаны дать одинаковый результат на четырёх состояниях
|
||
таблицы authority.
|
||
- Atomic add/dedup/rollback из #441 не меняются.
|
||
|
||
## Touch, клавиатура и доступность
|
||
|
||
Новых жестов и targets нет. Группа missing-space получает обычный
|
||
неинтерактивный текстовый заголовок и структурную связь со следующими строками;
|
||
состояние не передаётся только цветом. Tab order, действия строк, focus restore
|
||
и touch targets остаются прежними. Новая строка проходит en/ru/de/fr completeness
|
||
и locale smoke.
|
||
|
||
## Ошибки, гонки и крайние случаи
|
||
|
||
| Случай | Ожидаемое поведение |
|
||
|---|---|
|
||
| `map_routes: []` и непустой legacy `calibration` | 0 effective routes, legacy не читается |
|
||
| `map_routes: null` и legacy calibration | прежний legacy route |
|
||
| все routes отфильтрованы single-space export | в export остаётся `map_routes: []` |
|
||
| один valid и несколько missing routes | valid group первая, затем одна missing-space group со стабильными строками |
|
||
| отсутствуют все пространства routes | после draft area показывается только missing-space group; строки редактируемы |
|
||
| несколько роботов на разных этажах | subset содержит всех; каждый показывается только по своим route facts |
|
||
| roster меняется между HA updates | кадр использует одну immutable snapshot revision без смешения |
|
||
| нет captured snapshot | существующий fallback не падает и не фильтрует робота по dock space |
|
||
| malformed non-array `map_routes` | прежнее compatibility/fail-closed поведение без новой миграции |
|
||
|
||
## Acceptance criteria и доказательства
|
||
|
||
### AC1. Единая route authority
|
||
|
||
TS unit проверяет absent, `null`, `[]` и non-empty `map_routes` при наличии
|
||
legacy calibration. Пустой массив даёт ноль routes; непустой — только explicit;
|
||
absent/`null` сохраняют legacy.
|
||
|
||
### AC2. Backend parity
|
||
|
||
Python unit прогоняет ту же матрицу и сверяет нормализованные результаты с
|
||
frontend contract. Как минимум один отрицательный кейс обязан падать при
|
||
возврате старого условия «список непуст».
|
||
|
||
### AC3. Export не оживляет legacy
|
||
|
||
Backend import/export test экспортирует одно пространство из marker с explicit
|
||
routes, полностью удалёнными фильтром, и оставшимся legacy calibration.
|
||
Результат содержит `map_routes: []`; повторное вычисление effective routes после
|
||
round-trip даёт пустоту.
|
||
|
||
### AC4. Понятная и стабильная группа
|
||
|
||
Browser smoke открывает Device editor с valid и несколькими missing routes,
|
||
проверяет порядок групп/строк, единственный локализованный заголовок, warning и
|
||
доступность edit/delete. Повторный render и перестановка входного словаря spaces
|
||
при том же canonical order не меняют порядок. Pending draft остаётся отдельно.
|
||
|
||
### AC5. Renderer получает только роботов
|
||
|
||
Unit/contract test создаёт roster с не-vacuum и vacuum markers и проверяет, что
|
||
captured subset immutable, содержит только всех vacuum markers и используется
|
||
обычным `_renderVacuums` call site. Renderer не выполняет второй полный scan.
|
||
|
||
### AC6. Межэтажное и snapshot-поведение сохранено
|
||
|
||
Существующий multifloor vacuum smoke и cold/live continuity test остаются
|
||
зелёными: робот с базой на другом этаже отображается в route-space, hidden и
|
||
static suppression прежние, старые devices не смешиваются с новыми facts.
|
||
Mutation, возвращающий current-space `devs` в vacuum renderer, обязан уронить
|
||
межэтажный witness.
|
||
|
||
### AC7. Производительность не ухудшена
|
||
|
||
Large-house профиль 60 rooms / 200 devices на exact SHA проходит действующий
|
||
performance smoke и budget. Дополнительный structural witness фиксирует
|
||
`V < N` и один vacuum-only render pass; отдельный новый timing budget не
|
||
вводится, поскольку изменение сокращает сложность и не добавляет visual work.
|
||
|
||
### AC8. Совместимость и документация
|
||
|
||
Typecheck, unit и build зелёные. Перед бетой проходят golden, smoke и
|
||
performance по канону процесса; Linux CI является каноном полного HA harness.
|
||
`docs/VACUUM.md` явно называет пустой массив authority, оба changelog описывают
|
||
пользовательское исправление, en/ru/de/fr dictionaries проходят completeness.
|
||
|
||
## План тестирования
|
||
|
||
- расширить TS unit для `effectiveRoutes()` таблицей всех состояний поля;
|
||
- расширить backend unit `vacuum_routes` и import/export round-trip;
|
||
- добавить/расширить browser smoke vacuum route editor для missing-space group;
|
||
- расширить render snapshot unit/contract vacuum-only subset;
|
||
- сохранить и прогнать multifloor/cold-view vacuum smokes;
|
||
- обновить `scripts/mutation-gate.mjs`: explicit-empty mutant возвращает старую
|
||
проверку длины и краснеет; cross-floor mutant подменяет vacuum subset на
|
||
current-space devices и краснеет;
|
||
- в implementation loop запускать только `typecheck`, `unit`, `build`; golden,
|
||
smoke и performance запускать перед бетой согласно runbook.
|
||
|
||
## Карта реализации
|
||
|
||
- `src/vacuum-routes.ts` — array presence вместо non-empty selection;
|
||
- `custom_components/houseplan/vacuum_routes.py` — backend parity;
|
||
- `custom_components/houseplan/import_export.py` — сохранение explicit empty;
|
||
- `src/editors/vacuum-maps-section.ts` — partitions/groups и stable tie-breaker;
|
||
- `src/i18n/support/{en,ru,de,fr}.json` — заголовок missing-space group;
|
||
- `src/render-device-snapshot.ts` и `src/houseplan-card.ts` — immutable
|
||
vacuum-only subset и renderer call site;
|
||
- `test/`, `tests_backend/`, `demo/`, `scripts/mutation-gate.mjs` — witnesses;
|
||
- `docs/VACUUM.md`, `docs/CONFIG-COMPATIBILITY.md`,
|
||
`docs/USER-GUIDE.ru.md`, `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` —
|
||
release artifacts.
|
||
|
||
## Риски и rollback
|
||
|
||
- Главный compatibility risk — ошибочно отключить legacy fallback для absent или
|
||
`null`; его закрывает одинаковая четырёхстрочная матрица в TS и Python.
|
||
- Главный render risk — отфильтровать робота по текущему пространству/этажу;
|
||
subset строится глобально, а cross-floor mutant обязан краснеть.
|
||
- Главный snapshot risk — создать subset из другого поколения devices; список
|
||
формируется и замораживается вместе с основной snapshot revision.
|
||
- UI-группа может нарушить focus/order; smoke проверяет интерактивные строки, а
|
||
заголовок остаётся неинтерактивным.
|
||
- Rollback code path не требует data rollback: schema не меняется. Возврат
|
||
implementation не переписывает сохранённые конфигурации.
|
||
|
||
## Release-артефакты
|
||
|
||
- Обязательны записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же
|
||
user-visible коммите.
|
||
- Обновляется `docs/VACUUM.md` для явного empty-array/export контракта.
|
||
- Обновляется `docs/CONFIG-COMPATIBILITY.md`: раздел vacuum map routes явно
|
||
различает absent, `null`, explicit empty и non-empty `map_routes`, включая
|
||
сохранение `[]` при single-space export.
|
||
- Обновляется `docs/USER-GUIDE.ru.md`: раздел «Карты и этажи» описывает новую
|
||
видимую группу маршрутов «Пространство удалено» в Device editor.
|
||
- Изменение Device editor требует browser screenshot/smoke evidence в
|
||
light/dark темах; новый постоянный golden добавляется только если этот экран
|
||
уже входит в canonical golden surface, иначе review artifact прикладывается
|
||
к issue без расширения baseline.
|
||
- Перед бетой обязательны штатные golden, smoke и full performance artifacts;
|
||
новый security artifact не требуется.
|
||
|
||
## Принятые предположения
|
||
|
||
- Явный массив `map_routes` является authority независимо от длины — это уже
|
||
принятое решение #162, а не новый вопрос.
|
||
- Требование issue «явная группа» означает отдельный локализованный заголовок
|
||
после всех валидных пространств; действия строк не меняются.
|
||
- Исправление производительности ограничено устранением второго полного обхода:
|
||
отдельный новый latency threshold без подтверждённой деградации не нужен.
|
||
- `map_routes: null` остаётся legacy-compatible, чтобы не создавать скрытую
|
||
миграцию старых конфигураций.
|