Files
houseplan-card/docs/specs/107-virtual-light-toggle.md
T
2026-08-18 17:29:48 +00:00

583 lines
38 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.
# Issue #107 — переключение виртуального источника света «Всегда»
> **Уточнение #174:** это ТЗ остаётся authority для несвязанного ручного
> режима. Если на exact marker существует валидная входящая связь controller,
> HA state реальных drivers имеет приоритет, а клик по лампе выполняет HA group
> operation. Снятие последней связи возвращает сохранённый manual state; полный
> контракт описан в [ТЗ #174](174-linked-virtual-light-controller.md).
- **Issue:** https://github.com/Matysh/houseplan-card/issues/107
- **Редакция:** первая редакция для независимого ревью; статус задачи определяется
только метками issue
- **Тип / приоритет:** feature + polish / P2
- **Оценка:** пользовательская ценность 6/10; ценность для разработки 3/10;
сложность и риск 7/10
- **Область:** View-action, canonical light resolver, target preview, backend
operational state, multi-card live-sync, i18n, compatibility и тесты
- **Связано:** #65, #84, #88, #94, #97, `docs/SCOPE.md`,
`docs/TOUCH-SUPPORT.md`, `docs/LIGHT.md`,
`docs/CONFIG-COMPATIBILITY.md`
## 1. Сценарий и продуктовый контекст
**Персона:** home admin либо домочадец, который пользуется обычным View на
настенной панели, телефоне или desktop dashboard. Право редактировать план для
этого действия не требуется.
**Поверхность и момент:** на плане есть virtual marker, у которого роль источника
света явно установлена в «Всегда», а действие по нажатию — «Переключить
состояние». Пользователь нажимает marker, чтобы погасить или снова включить
изображённый виртуальный светильник.
Задача обслуживает:
- **J1:** Glow, room fill и room light statistics показывают одно текущее
пространственное состояние;
- **J3:** очевидное безопасное действие выполняется прямо с плана;
- гарантированный touch/View/kiosk-контракт: tap работает так же, как click, и
результат не расходится между настенной панелью, телефоном и второй карточкой.
Это утверждённое владельцем узкое исключение к замороженной функциональности
virtual devices. Оно не создаёт общего state engine для виртуальных маркеров и
не меняет их роль placeholder в остальных комбинациях.
## 2. Что человек увидит до и после
**До:** диалог честно сообщает, что переключаемого состояния нет, а нажатие в
View является тихим no-op. Passive `Always` source без контроллера всегда
считается включённым по контракту #84.
**После:** в точной утверждённой комбинации preview показывает сам виртуальный
источник, его текущее состояние и следующий эффект. Нажатие выключает его, а
повторное включает. Одновременно меняются его Glow и все room-light consumers.
Результат сразу видят другие карточки и он сохраняется после reload страницы и
restart Home Assistant.
## 3. Подтверждённая причина текущего поведения
1. `resolvedLightSources()` создаёт для passive `Always` источник с identity
`marker:<id>`, пустыми `stateEids`/`serviceEids` и default `on`.
2. Если сохранены incoming controller links, #84 заменяет default на OR их HA
driver entities. Ручного состояния у источника нет.
3. `resolveToggleIntent()` ищет только HA service target. Virtual marker без
actionable entity получает `no-actionable-entity`.
4. `_clickDevice()` выполняет только HA service command и при его отсутствии
намеренно ничего не делает.
5. Hint в диалоге форматирует тот же resolved intent и поэтому показывает
`marker.toggle_none_no_actionable_entity`.
Локальная ветка только в renderer не подходит: #84 закрепил
`resolvedLightSources()` как authority для Glow, room fill, room state,
`N из M`, room card и preview. Локальный browser state также нарушил бы принятое
решение о reload/restart и multi-card synchronization.
## 4. Решения владельца
Владелец принял defaults Q1–Q3 14.08.2026. Каноническая запись:
https://github.com/Matysh/houseplan-card/issues/107#issuecomment-5287719167
1. Состояние общее для всех карточек, live-синхронизируется, переживает reload
страницы и restart Home Assistant. Источник без сохранённой записи начинает
в `on`, сохраняя совместимость с #84. State является operational data и не
входит в конфигурацию плана или export.
2. Переключается canonical state источника, а не один SVG Glow. Вместе меняются
Glow, room light fill, room statistics/card и preview. Геометрия, цвет,
яркость и радиус не меняются.
3. Пока действует точная тройка `binding=virtual` + `is_light=true` + effective
`tap_action=toggle`, ручное состояние авторитетно; incoming controller links
сохраняются, но не управляют этим источником. Когда тройка перестаёт
действовать, ручная запись удаляется и немедленно возвращается обычная
семантика #84. Повторное включение тройки начинает с `on`.
## 5. Точное условие исключения
Manual virtual-light action существует только для активного marker, у которого
одновременно выполняется:
1. `marker.binding === 'virtual'`;
2. `marker.is_light === true`;
3. effective tap action равен `toggle`;
4. marker не является удалённым tombstone (`removed !== true`).
Для virtual marker нет HA domain, который мог бы неявно спроецировать default
`toggle`, поэтому сохранённый action token обязан быть `toggle`. Backend всё
равно проверяет persisted config, а не доверяет утверждению frontend.
`hidden: true` временно убирает источник из визуальных consumers, но не стирает
его ручное состояние: возврат видимости восстанавливает прежний `on/off`.
В точной тройке manual target имеет приоритет действия. Tap переключает только
состояние собственного virtual source и не вызывает HA service даже при наличии
сохранённых outgoing `controls`. Сами `controls` остаются lossless и продолжают
участвовать в light graph по правилам #65/#84. После выхода из тройки обычный
resolver #94 снова получает их без изменения.
Все прочие сочетания — virtual Auto/Never, non-virtual Always, другое действие,
удалённый marker — сохраняют текущее поведение без нового state target.
## 6. Нормативная модель состояния
### 6.1. Значение по умолчанию
Отсутствующая запись означает `on`. Поэтому существующие passive `Always`
источники не меняют вид после обновления, а первый tap переводит источник в
`off`.
Хранилище может сохранять только множество выключенных marker IDs: отсутствие
ID является canonical `on`. Это техническая форма, а не новый пользовательский
режим. В ответах frontend получает revisioned snapshot, достаточный для
однозначного восстановления `on/off`.
### 6.2. Один canonical consumer contract
Для eligible marker manual state подменяет только вычисление `source.on`.
Identity, room ownership, position, `castsGlow`, geometry, manual/live colour,
brightness и radius остаются теми же.
Один и тот же resolved source обязан дать одинаковый результат для:
- spatial Glow full card и kiosk;
- light room fill;
- room light state и `resolvedLightStats` / `N из M`;
- room card и device presentation;
- device-editor preview;
- статического `houseplan-space-card`.
Нельзя добавлять manual-state ветку отдельно в SVG renderer, room card,
preview или static card. Runtime snapshot является входом canonical light
resolver, а его revision входит в cache invalidation. Один tap не должен ждать
следующего HA state tick.
### 6.3. Controller semantics
- Для eligible source manual `on/off` выигрывает у incoming controllers.
- Incoming `marker:<id>` links не удаляются, не становятся service targets и не
меняют HA entities.
- Контроллер продолжает выполнять собственное действие #94, но его HA state не
меняет manual source, пока тройка активна.
- После выхода target из тройки manual record очищается; incoming links снова
определяют passive source по OR-контракту #84 без Save со стороны controller.
- Остальные источники того же room продолжают давать независимые votes. Поэтому
выключенный manual source не обязан погасить комнату, если в ней включён другой
источник.
## 7. Backend storage и WebSocket contract
### 7.1. Отдельные operational data
State хранится в отдельном versioned Home Assistant `Store`, например
`houseplan.virtual_lights`, а не в `Marker`, layout, Lovelace config,
localStorage-authority или HA entity registry.
Минимальная логическая структура:
- monotonic runtime revision;
- последняя согласованная config revision;
- bounded set/map выключенных eligible marker IDs.
Точные Python class/key names можно изменить. Обязательны отсутствие записи =
`on`, лимит не выше числа markers, отказ от произвольных строк вне live config и
отдельность от export/import payload.
### 7.2. Initial snapshot
`houseplan/config/get` возвращает дополнительное optional поле с revisioned
virtual-light snapshot. Оно читается вместе с той же config revision, по которой
backend проверил eligibility.
Это поле является обязательной частью корректного нового frontend frame:
сохранённый `off` не может сначала отрисоваться как `on` и исправиться только
после необязательной подписки. Старый frontend игнорирует добавочное поле.
Поле optional на wire только для rolling/downgrade compatibility. Новый frontend
при его отсутствии применяет legacy/default `on`; попытка toggle на старом
backend завершается контролируемой ошибкой, без optimistic ложного состояния.
### 7.3. Atomic toggle command
Добавляется один authenticated WS command вида
`houseplan/virtual_light/toggle`, принимающий только `marker_id`.
Backend под общим для config/state load-modify-save lock:
1. загружает текущую config revision и marker;
2. повторно проверяет точное условие §5;
3. читает текущее значение (`on`, если записи нет);
4. атомарно инвертирует его и сохраняет state revision;
5. возвращает `{marker_id, on, rev}`;
6. после успешного save публикует `houseplan_virtual_light_updated` с тем же
payload.
Клиент не присылает желаемый boolean и не выполняет read→set: две одновременные
команды сериализуются как два toggle, а не теряют одну запись. Event не
публикуется до durable save. Invalid/missing/ineligible marker получает
стабильную ошибку `not_toggleable`; store и event при этом не меняются.
### 7.4. Права
Команда доступна любому аутентифицированному HA-пользователю, которому доступна
карточка. `may_write` и опция admin-only защищают редактирование плана, но не
View-action; иначе утверждённая household/kiosk persona увидела бы рабочий hint
и получила no-op.
Это разрешение уже, чем HA service toggle: request содержит только marker
ID, backend разрешает ровно утверждённую тройку и не вызывает HA service. Guest
без authenticated HA session доступа не получает.
### 7.5. Config coherence и cleanup
Каждый backend writer конфигурации — обычный Save, import apply, optimize/undo,
setup migration и другие пути замены config — согласует operational store с
новой config revision до публикации завершённого config update:
- сохраняет `off` только для marker IDs, которые всё ещё eligible;
- удаляет записи deleted/rebound/Auto/Never/non-toggle markers;
- сохраняет state при rename, move, room/space reassignment, hidden/unhidden и
несвязанных edits;
- записывает согласованную config revision.
Из-за двух отдельных HA Store файлов crash между saves не может быть общей
транзакцией. Fail-safe правило: если operational store видит неизвестный разрыв
config revision (включая downgrade, старый writer или прерванный save), он
сбрасывает manual-off overrides к безопасному compatibility default `on`, а не
рискует воскресить старый `off` после повторного включения тройки. Следующий
snapshot сохраняет согласованную revision.
Все config writers должны использовать один backend helper; список разрозненных
ручных `state_store.async_save()` не допускается.
## 8. Frontend action и synchronization
### 8.1. Единый resolved intent
Authority #94 расширяется typed operational target, а не обходится ранним
`if` непосредственно в `_clickDevice()` или диалоге.
Для eligible marker resolver возвращает:
- stable target identity `marker:<id>` и читаемое имя marker;
- current `on/off` из принятого runtime snapshot;
- next effect `turn-off` / `turn-on`;
- executable operation `virtual-light-toggle`, отличную по типу от HA service
command.
Formatter preview/confirmation получает эту же модель. Virtual target никогда
не выдаётся за `entity_id` и никогда не попадает в `callService`.
### 8.2. Tap, click и confirmation
- Обычный tap/click отправляет atomic WS toggle и принимает только response/event
с revision не старее локального.
- Optimistic визуальный flip запрещён: transport/backend failure не должен
показывать несохранённое состояние.
- `tap_confirm: true` сохраняет существующий confirmation dialog. Перед
выполнением eligibility/target re-resolve повторяется; текущий direction
решает серверный atomic toggle.
- Если marker перестал быть eligible до подтверждения или ответа, action не
retargets на controls/HA entity. Пользователь получает существующий смысл
«цель изменилась» либо локализованную контролируемую ошибку.
- Ошибка WS использует локализованный toast и оставляет последний подтверждённый
state. Page error и unhandled rejection недопустимы.
### 8.3. Live-sync и несколько карточек
Full card подписывается на `houseplan_virtual_light_updated` рядом с текущими
House Plan events. `houseplan-space-card` получает тот же snapshot/event через
module-level shared cache, чтобы N static cards не создавали N fetch/subscription.
Monotonic revision защищает от перестановки response и event. Применение более
новой revision:
1. заменяет immutable runtime snapshot;
2. инвалидирует light/presentation cache;
3. обновляет local server-snapshot cache;
4. просит render без structural config/device rebuild и без HA state mutation.
Missed event после disconnect исправляется следующим `config/get`. Подписка
best-effort не заменяет initial snapshot и не является условием первого полного
кадра.
### 8.4. Local cache
`LS_CFG` может хранить последний revisioned operational snapshot рядом с
config/layout для мгновенного stale-while-revalidate кадра. Он не становится
authority: server response всегда выигрывает по revision/config coherence.
Старый cache без поля означает default `on`. Corrupt/oversized/wrong-shape state
игнорируется целиком, а не частично внедряется в resolver. Runtime event обновляет
cache, чтобы reload не давал краткого обратного Glow flash.
## 9. UX редактора и i18n
В точной тройке прежний текст
`marker.toggle_none_no_actionable_entity` не показывается. Вместо него preview
содержит локализованные строки:
- target: виртуальный источник с именем marker, без вымышленного entity ID;
- current state: «включён/выключен»;
- next effect: «включить/выключить».
Новый несохранённый draft и draft, который только что вошёл в тройку, показывают
initial `on → off`. Редактируемый сохранённый marker показывает server state.
Cancel не меняет operational state; Save меняет eligibility и запускает cleanup
только после принятого config write.
Нужны полноценные RU/EN keys для virtual target/state и ошибки toggle. Нельзя
склеивать предложения или показывать backend error text как готовый перевод.
Существующие HA entity/group/secure/missing hints #94 не меняются.
## 10. Accessibility и touch
Новых controls и focus order нет. Existing device marker, selector,
confirmation и toast сохраняют текущие accessible surfaces.
Touch View и kiosk — блокирующие поверхности:
- один tap выполняет ровно один toggle;
- pan, pinch, long-press и suppressed synthetic click не выполняют action;
- confirmation остаётся доступным и не теряет focus contract;
- live event не перехватывает focus и не создаёт screen-reader spam;
- static card остаётся неинтерактивной, но показывает тот же state.
Touch editor остаётся best effort: новый control не добавляется, а существующий
desktop-first preview должен оставаться читаемым без горизонтального overflow.
## 11. Lifecycle и edge cases
1. Existing eligible marker без state entry: `on`; первый tap → `off`.
2. Повторный tap: `off` → `on`, off-entry может быть удалена.
3. Reload/new browser/restart HA: последнее durable значение восстановлено.
4. Две full cards и static card: одно событие приводит все к одной revision.
5. Два одновременных tap: две server revisions, итог равен исходному состоянию.
6. Response пришёл после более нового event: stale response игнорируется.
7. Incoming controller включён, manual source выключен: source остаётся `off`.
8. Manual source выключен, другой room source включён: room остаётся `on`, stats
считают голоса раздельно.
9. Outgoing controls есть: manual tap не вызывает их HA services; config
сохраняется lossless.
10. `Always → Auto/Never`: off-entry очищена, #84/#65 действует сразу.
11. `toggle → info/run/more-info`: запись очищена, новое действие штатное.
12. Virtual → HA binding/rebind: запись очищена и не приклеивается к новой цели.
13. Delete/tombstone: запись очищена; restore/recreate начинает `on`.
14. Rename/move/room/space/hidden: state сохраняется по stable marker ID.
15. Full import сохраняет state только для совпавших всё ещё eligible IDs;
импорт не приносит state из backup.
16. Config revision gap/downgrade writer: conservative reset к `on`, без stale
resurrection.
17. Backend unavailable/command failure: confirmed state остаётся, generic no-op
не маскирует ошибку.
18. Old frontend + new backend: extra snapshot/event игнорируются, config не
повреждается; marker ведёт себя по старому.
19. New frontend + old backend: default `on`, команда не создаёт local false
state и заканчивается контролируемой ошибкой.
20. Auto/Never/non-virtual/missing marker request, подделанный вручную: backend
`not_toggleable`, без save/event/service.
## 12. Модель данных, compatibility и import/export
`Marker`, `ServerConfig`, layout и Lovelace card config не получают новых полей.
Backend `CONFIG_SCHEMA`, import schema и config revision не меняются. Миграция
плана и materialization отсутствуют.
Создаётся отдельный optional operational Store version 1. Его отсутствие при
upgrade/downgrade означает пустой off-set/default `on`. Старые integration и
frontend его не читают и не уничтожают plan config. Config revision gap после
старого writer обрабатывается fail-safe правилом §7.5.
Operational state намеренно не входит в:
- full/space export и import preview/apply;
- marker JSON и compatibility passthrough;
- Lovelace config/local editor draft;
- HA entity/device registry.
Диагностика может показывать version/revision/count, но не обязана экспортировать
полную map marker IDs. `docs/CONFIG-COMPATIBILITY.md` фиксирует wire field,
отдельный Store, old/new matrix и reset-on-unknown-revision.
## 13. Не входит в задачу
- общее переключаемое состояние для virtual devices;
- действие для virtual Auto/Never либо для `info`, `run`, `more-info`;
- создание `input_boolean`, synthetic HA light/entity или helper;
- управление цветом, brightness, radius, geometry или animation через tap;
- запись state в marker config, layout или export backup;
- изменение OR/links semantics #84 вне manual-authority исключения;
- переключение HA entities из exact manual action;
- история состояний, расписания, automations или restoration UI;
- новый editor control «начальное состояние»;
- polling, remote API либо отдельная general-purpose runtime-state platform.
## 14. Критерии приёмки
- **AC1 (`unit`):** eligibility matrix разрешает operational target только для
active `virtual + is_light:true + explicit/effective toggle`; Auto, Never,
non-virtual, other action и removed сохраняют прежний resolver #94.
- **AC2 (`backend`):** новый eligible marker без записи возвращает `on`; atomic
toggle сохраняет `off`, второй сохраняет `on`, увеличивает revision и
публикует event только после успешного durable save.
- **AC3 (`backend`):** два concurrent toggle одного marker сериализуются без
lost update; response/event revisions монотонны, итог соответствует двум
инверсиям.
- **AC4 (`backend` + `ревью кода`):** non-admin authenticated connection может
выполнить eligible toggle без `may_write`; forged missing/ineligible ID
получает `not_toggleable`, не меняет store и не вызывает HA service.
- **AC5 (`unit`):** manual `off` является `source.on=false` во всех canonical
light consumers; incoming controller `on` его не перекрывает, а другой
независимый room source продолжает влиять на room aggregate.
- **AC6 (`unit` + `ревью кода`):** exact manual intent имеет typed marker target,
current/next effect и operational operation; marker ID никогда не попадает в
`hass.states` как entity и в `callService`/`entity_id`.
- **AC7 (`unit` + `smoke`):** диалог exact-комбинации не показывает
`toggle_none_no_actionable_entity`, показывает virtual target и current/next;
остальные hints/confirmation #94 не изменились.
- **AC8 (`backend`):** restart/recreation runtime data загружает сохранённый
`off`; config/get возвращает согласованный revisioned snapshot, достаточный
для первого `off` frame без ожидания event.
- **AC9 (`backend`):** все config writers сохраняют state для rename/move/
hidden/unrelated edits и очищают его при role/action/binding/delete change;
неизвестный config revision gap сбрасывает overrides к `on`.
- **AC10 (`smoke`):** tap/click в full View меняет Glow, room fill и room
`N из M` вместе; повторный tap возвращает их. Touch pointer выполняет один
toggle, pan/pinch/long-press не выполняют.
- **AC11 (`smoke`):** две full cards и `houseplan-space-card` принимают один
server event и показывают одинаковый state; stale response не откатывает
более новую revision, reconnect/reload исправляет missed event.
- **AC12 (`unit` + `smoke`):** outgoing controls exact marker не получают HA
service от manual tap; после выхода из тройки прежний resolver #94 получает
сохранённые controls без потери.
- **AC13 (`unit` + `smoke`):** `tap_confirm` re-resolves stable marker target,
выполняет server-current atomic toggle и безопасно отказывает, если eligibility
изменилась до подтверждения.
- **AC14 (`unit` + `ревью кода`):** runtime revision инвалидирует canonical
light/presentation cache без HA tick, polling и structural config/device
rebuild; N static cards разделяют cache/subscription.
- **AC15 (`unit` + `build`):** RU/EN parity, malformed cache/store input и
old/new optional field matrix покрыты; три bundle snapshot после build
побайтно совпадают.
- **AC16 (`ревью кода`):** оба changelog, RU user guide, README light recipe,
LIGHT, ARCHITECTURE и CONFIG-COMPATIBILITY описывают точное исключение,
persistence/permissions и отсутствие export.
## 15. План тестов и доказательств
### 15.1. Frontend unit
1. Eligibility/action matrix AC1/AC6/AC12.
2. Canonical source matrix: default on, manual off/on, controller OR ignored,
other room sources, hidden and exit from triple.
3. Intent formatter: virtual target/current/next, no fake entity ID, no old
no-actionable text, confirmation target stability.
4. Runtime snapshot cache: revision ordering, event-before-response,
reconnect snapshot and malformed local cache.
5. Resolver-cache mutant: removing runtime revision from cache key must make a
test fail because state would stay stale without HA tick.
### 15.2. Backend
1. Store absence/default, off/on persistence and runtime recreation.
2. Atomic concurrency with controlled interleaving; save/event ordering.
3. Admin and non-admin authenticated connection; invalid marker matrix.
4. Config writer lifecycle for ordinary set, import apply, optimize/undo and
setup migration path.
5. Config revision gap, corrupt/oversized Store, bounded pruning and save
failure without false event.
6. Wire compatibility: optional snapshot, old client payload and no config
schema/export changes.
### 15.3. Browser smoke
Targeted scenario builds one eligible virtual lamp, one incoming controller,
one unrelated room source, two full cards and one static card. It checks
AC7/AC10–AC13 using real click and touch pointer sequences, visible Glow/room
presentation, WS call log, event revisions and zero HA service calls.
Reload starts from server `off` and proves the first committed candidate is
already off. A disconnected card misses an event, reconnects and adopts the
current snapshot. Confirmation race changes config before confirm and proves no
retarget.
По действующему процессу smoke пишется вместе с реализацией, но в цикле
реализации запускаются только typecheck, unit и build. Целевой smoke и полный
browser smoke исполняются перед бетой; reviewer может запустить узкий сценарий
для доказательства AC.
### 15.4. Golden и performance
Golden matrix получает точный on/off virtual-lamp fixture на существующей flat
View сцене; новый художественный baseline не проектируется. Golden verify и
обновление принятого baseline выполняются только на pre-beta gate.
Performance gate должен подтвердить отсутствие polling, дополнительного HA
state subscription, per-source network call и полного config/device rebuild на
event. Отдельный новый численный budget не вводится: операция пользовательская,
state snapshot bounded числом markers, а resolver остаётся frame-cached.
## 16. Security и надёжность
Основной новый trust boundary — authenticated non-admin WS mutation. Он
ограничен следующими правилами:
- marker выбирается только из server config;
- eligibility вычисляет backend;
- command не принимает state, entity ID, service/domain или config fragment;
- payload и persisted set ограничены существующим marker limit;
- одна команда делает одну atomic inversion;
- никакой HA service/file/network action не выполняется;
- failed save не создаёт success response/event;
- revision gap деградирует в `on`, а не в stale manual state.
Lock/alarm invariant `docs/SCOPE.md` не затронут: virtual operational target не
может резолвиться в secure entity или HA service.
## 17. Release-артефакты
Реализационный пользовательский коммит содержит:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #107;
- `docs/USER-GUIDE.ru.md` — рецепт точной тройки, persistence и отличие от
controller link;
- `README.md` — краткий EN recipe/contract;
- `docs/LIGHT.md` — manual authority в canonical resolver;
- `docs/ARCHITECTURE.md` — operational Store/WS/live-sync boundary;
- `docs/CONFIG-COMPATIBILITY.md` — old/new matrix, config-revision coherence и
export exclusion;
- frontend unit, backend tests и browser scenario;
- синхронные `dist/houseplan-card.js`,
`custom_components/houseplan/frontend/houseplan-card.js` и
`demo/srv/assets/houseplan-card.js`.
Новых migration UI, screenshot документации и отдельного security report не
требуется. Перед бетой идут штатные golden verify, browser smoke и performance
gate по release runbook.
## 18. Откат
Откат — обычный revert frontend/backend изменения, тестов, документации,
changelog и bundle snapshot. Plan config/layout/import formats не меняются.
Отдельный operational Store после downgrade безопасно игнорируется; старая
версия снова считает passive `Always` source включённым и не умеет его
переключать. При последующем upgrade незнакомый config revision сбрасывает stale
overrides к `on`. Ручное редактирование `.storage` пользователю не предлагается.
## 19. Принятые технические предположения — можно менять без пересмотра продукта
1. Отдельный Store и хранение только off IDs предпочтительны, но точный key и
Python class names не являются продуктовым решением.
2. Initial state может ехать дополнительным полем `config/get` либо другим
атомарным snapshot endpoint, если первый frame, old/new compatibility и
config-revision coherence полностью сохранены.
3. Typed intent можно оформить union-командой либо отдельным operational action
рядом с HA command; запрещены только две расходящиеся resolver-ветки.
4. Full card может иметь собственную event subscription, а static cards — общую
module-level; точная раскладка допустима при отсутствии N одинаковых fetch.
5. Local cache хранит snapshot для continuity, но server revision остаётся
authority и optimistic toggle не вводится.
6. Config writer helper может консервативно сбросить все manual overrides при
неизвестном revision gap вместо попытки угадать историю eligibility.
7. Hidden marker сохраняет state, tombstone удаляет; это следует существующей
границе visual hide против semantic delete.
8. Full import не импортирует state, но может сохранить текущее значение
совпавшего stable eligible marker ID после reconciliation.