# Issue #98 — единая система пульсаций и активности устройства - **Issue:** https://github.com/Matysh/houseplan-card/issues/98 - **Статус ТЗ:** реализовано локально; ожидает prerelease/CI gate - **Приоритет:** P1 / polish - **Оценка:** сложность 7/10, пользовательская ценность 7/10, ценность для разработки 8/10, score `(2×7+8)/7 = 3,14` - **Риск:** средне-высокий — общий renderer, finite edge runtime, alarm safety и parity трёх surfaces меняются одновременно - **Область:** frontend presentation, runtime событий, общий renderer устройства, editor preview, static card, i18n, accessibility, документация и QA - **Модель данных:** без изменений - **Связано:** #22 (pending/click feedback), #73 (visual continuity), #90 (value badge), #31 (полная View accessibility), `docs/LIGHT.md`, `docs/ARCHITECTURE.md` - **Touch:** View/kiosk полностью поддерживаются; device editor остаётся desktop-first, его preview на touch — best effort по `docs/TOUCH-SUPPORT.md` ## 1. Резюме House Plan должен использовать один понятный визуальный язык для динамики устройства. Вокруг сохранённого marker допускаются ровно три вида анимированной индикации: 1. **Тревога** — красная непрерывная пульсация, пока активно критическое состояние. 2. **Короткое событие** — три конечные волны общей длительностью около 3,3 с. 3. **Постоянная активность** — спокойная непрерывная пульсация, пока устройство действительно работает, перемещается или обнаруживает присутствие. Статичных колец больше нет. При `prefers-reduced-motion: reduce` обычная активность обозначается компактной сплошной точкой внутри подложки, а тревога остаётся понятной по красной подложке и доступному описанию. Семантические причины `event / presence / transition / running` сохраняются. Они проецируются общим pure resolver в визуальные `none / alarm / short / continuous`. Один renderer используется на интерактивном плане, в `hp-device-preview` и в `houseplan-space-card`. Хранимые значения `display`, `ripple_color` и `ripple_size` не меняются. Старый `display: ripple` продолжает читаться как `icon_ripple`. ## 2. Проблема текущей реализации Сейчас единая пользовательская идея реализована несколькими путями: - `device-visual.ts` классифицирует semantic activity; - `houseplan-card.ts` и `space-card.ts` отдельно держат finite runtime; - `device-presentation.ts` скрывает обычную activity для части display modes; - `device-face.ts` рисует `.activity-ring`; - тревога рисуется отдельно через `.dev.alarm::after`; - presence является статичным кольцом; - reduced motion превращает все activity-эффекты в статичное кольцо; - preview умеет показать только один абстрактный event-demo; - UI называет параметры «Цвет/Размер эффекта», не объясняя их область действия. Из-за этого визуальная система расходится по типам устройств и surfaces. Пользователь не может предсказать результат выбранной опции, а новый класс устройства приходится вручную подключать к нескольким renderer/CSS-веткам. ## 3. Цели 1. Один нормативный pipeline от HA state до визуального pulse. 2. Ровно три динамических pulse-kind без статичных колец. 3. Одинаковый результат на полном плане, в preview и static card. 4. Чёткие названия display modes и контекстные подсказки. 5. Предсказуемые приоритеты alarm, short event и continuous activity. 6. Безопасный reduced-motion fallback без мигающих/движущихся колец. 7. Полная совместимость существующей конфигурации. 8. Детерминированные runtime, smoke и golden проверки. ## 4. Не входит в задачу - новые правила выбора `primary`, binding или visual sources; - новые классы устройств сверх уже распознаваемой семантики; - optimistic/pending feedback между кликом и ответом HA — #22; - изменение Glow, лучей из окон, room fill и light-source resolver; - анимация live vacuum puck и его следа; - boot/recovery/navigation animations; - пользовательская настройка скорости pulse; - изменение config schema и переименование `ripple_*` в storage; - автоматическое включение activity mode у существующих marker; - отдельные цвета/размеры для presence, transition и running. ## 5. Термины и нормативные типы ### 5.1. Semantic activity Существующий тип остаётся источником причины: ```ts type DeviceActivity = | 'none' | 'event' | 'presence' | 'transition' | 'running'; ``` - `event` — наблюдённый короткий edge; - `presence` — устойчивое обнаружение присутствия; - `transition` — фактическое механическое движение либо короткий terminal fallback; - `running` — устойчивая полезная работа. ### 5.2. Rendered pulse Вводится визуальная проекция: ```ts type DevicePulseKind = 'none' | 'alarm' | 'short' | 'continuous'; type DevicePulseReason = | 'alarm' | 'event' | 'presence' | 'transition' | 'running'; interface ResolvedDevicePulse { kind: DevicePulseKind; reason: DevicePulseReason | null; generation: number; color: string | null; diameterScale: number; expiresAt: number | null; reducedMotionIndicator: 'none' | 'dot'; } ``` `kind` определяет DOM/CSS. `reason` сохраняется для preview, diagnostics, accessible description и будущих расширений, но не создаёт четвёртый вид анимации. `generation` и `expiresAt` имеют смысл только для `short`: continuous/alarm не должны перезапускать CSS timeline из-за обычного HA re-render. `diameterScale` назван отдельно от масштаба самого marker и означает максимальный диаметр волны в диаметрах подложки. ## 6. Источники семантики ### 6.1. Тревога Тревога активна, пока хотя бы один разрешённый critical source имеет критическое состояние: - `binary_sensor` device class: `smoke`, `gas`, `carbon_monoxide`, `moisture`, `safety`, `tamper`, `problem`; - `siren` в активном состоянии; - `alarm_control_panel: triggered`; - остальные случаи, уже канонически возвращаемые `isAlarmState()`. ТЗ не расширяет `isAlarmState()`. Любое последующее расширение делается в семантическом resolver и автоматически получает общий alarm pulse. ### 6.2. Короткое событие Short pulse запускается только от наблюдённого edge после установленного baseline: | Источник | Условие | | --- | --- | | motion/vibration/sound | валидный `off → on` | | door/window/garage/opening contact | валидный `off → on` | | button/event | изменение валидного state/event token | | stateless action House Plan | подтверждённый успешный service call | | cover | прямой `closed ↔ open` без наблюдаемого opening/closing | | lock | прямой `locked ↔ unlocked` без locking/unlocking | | valve | прямой `closed ↔ open` без opening/closing | Если в одном HA snapshot обнаружены и ordinary event, и terminal fallback, выбирается `event`: порядок обхода сущностей и registry rows не влияет на результат. Между разными snapshot более новый валидный short edge перезапускает цикл целиком. Событие не создаётся: - на первом snapshot; - при `unknown/unavailable/missing → value`; - при reconnect/resume/rebuild source graph; - при неуспешном, отменённом или неподдерживаемом service call; - от одного только hover, focus, preview re-render или смены display mode. ### 6.3. Постоянная активность Continuous pulse существует ровно пока semantic state активен: | Причина | Примеры | | --- | --- | | presence | `occupancy/presence = on` | | transition | cover/lock/valve `opening/closing/locking/unlocking`; moving sensor; vacuum returning | | running | light/switch/fan/humidifier `on`; running/power binary sensor; climate heating/cooling/preheating/defrosting; vacuum cleaning; script on; распознанные working states техники | | controls | хотя бы один выбранный canonical visual/control source реально работает | Ограничения: - `automation: on` означает enabled, а не running; - `media_player` не получает continuous pulse от `on/playing/paused`; - `cover` open/closed остаётся glyph-state, а не working status; - `unavailable/unknown` снимает обычную activity; - transition заканчивается сразу при terminal state, без доведения до 3,3 с; - terminal fallback, наоборот, является short pulse и ограничен finite runtime. ## 7. Приоритеты и projection Нормативный порядок: 1. удалённый/effective hidden marker не рендерится; 2. HA-disabled design preview и orphaned binding не имеют pulse; 3. `static_icon` возвращает `none` даже при alarm; 4. alarm возвращает `alarm` и перекрывает всё; 5. если `live_states === false`, обычный pulse отсутствует; 6. finite event runtime возвращает `short`; 7. актуальная `presence/transition/running` возвращает `continuous`; 8. иначе `none`. `unavailable` не является device-level veto само по себе. Resolver сначала отбрасывает недоступные samples: активный critical source другого entity всё ещё обязан показать alarm, а доступный рабочий source — continuous. Pulse отсутствует из-за availability только когда после фильтрации не осталось ни одного доступного релевантного source. Это сохраняет действующий контракт `combineVisualSamples()` и не позволяет одному диагностическому entity погасить реальную тревогу устройства. Short pulse имеет приоритет над continuous. После окончания short resolver снова проверяет актуальную семантику и без паузы возвращает continuous, если устройство продолжает работать. Если во время terminal fallback появляется реальный transition state, finite fallback сбрасывается и сразу заменяется continuous transition. При первом входе в alarm ordinary finite runtime **очищается**, а текущие samples становятся новым baseline. Замораживание запрещено: после снятия тревоги старый short не возобновляется, но новый валидный edge, произошедший уже после alarm, обрабатывается обычно. ## 8. Матрица display modes Хранимые токены не меняются. `badge` остаётся default. | Config | RU | EN | | --- | --- | --- | | `badge` | Значок + состояние | Icon + state | | `icon_ripple` | Значок + состояние и активность | Icon + state and activity | | `value` | Значение + состояние | Value + state | | `static_icon` | Всегда статичный значок | Always static icon | ### 8.1. Полная матрица | Режим | Нейтрально / unavailable | Working/open state | Short | Continuous | Alarm | Satellites | | --- | --- | --- | --- | --- | --- | --- | | Значок + состояние | текущая тёмная/приглушённая подложка | жёлтая working, оранжевая physical open; cover меняет glyph | нет | нет | красная подложка + alarm pulse | icon/morph, value badge и LQI разрешены | | Значок + состояние и активность | как выше | как выше | три волны | единая спокойная пульсация | красная подложка + alarm pulse | как выше | | Значение + состояние | динамическая подложка | жёлтая/оранжевая подложка | нет | нет | красная подложка + alarm pulse | value вместо icon, fallback icon, value badge/LQI | | Всегда статичный значок | всегда нейтральная тёмная подложка | без изменений | нет | нет | нет | только базовый icon; без morph/value/value badge/LQI/RGB/live-vacuum overlays | Alarm является safety-исключением для всех state-aware modes. Только явный `static_icon` скрывает alarm presentation; диалог сохраняет предупреждение для alarm-capable binding. Hover, focus outline, shadow и click action не входят в display projection и остаются одинаковыми по действующему контракту. ## 9. Визуальный контракт ### 9.1. Alarm - системный цвет `#f25a4a` либо единый design token с тем же контрастом; - красная plate сохраняется; - один expanding/fading pulse, непрерывно пока alarm активен; - пользовательские `ripple_color/ripple_size` игнорируются; - интенсивность не усиливается при hover; - частота/opacity не создают WCAG flashing violation. ### 9.2. Short - ровно три последовательные волны; - одна волна 1,1 с, total runtime 3,3 с; - повторный edge увеличивает generation и перезапускает весь цикл; - старый DOM/timer не накапливается; - цвет и максимальный диаметр берутся из ordinary activity settings. ### 9.3. Continuous - один breathing pulse без статичной фазы; - один общий tempo для presence, transition и running: рекомендуемо 2,4 с `ease-in-out infinite`; - reason не меняет геометрию, скорость и интенсивность; - выключение/idle/terminal state снимает pulse немедленно; - смена причины `presence → running` не должна визуально вспыхивать или перезапускать timeline, если kind остаётся `continuous`. ### 9.4. Цвет и размер ordinary activity Порядок цвета: 1. валидный `marker.ripple_color`; 2. текущий RGB canonical light source; 3. `--hp-accent`. Размер: `marker.ripple_size`, default `3`, текущие backend limits сохраняются. Параметры применяются только к `short/continuous`, не к alarm и не к reduced-motion dot. ## 10. Reduced motion и accessibility При `prefers-reduced-motion: reduce`: - `.device-pulse` с движущимися кольцами не рендерится либо полностью скрыт; - alarm читается по красной plate и accessible state; - working/open читаются по существующим plate/glyph states; - в `icon_ripple` short/continuous отображаются `.activity-dot`; - short dot живёт то же finite окно 3,3 с; - continuous dot существует до окончания semantic state; - у других display modes dot отсутствует; - статичное outline/ring запрещено. Dot находится **внутри нижнего правого угла plate**, а не снаружи marker: - не конфликтует с `new-device` dot сверху справа; - не увеличивает bounding box; - не пересекается с внешним value badge и нижним LQI; - имеет контрастную 1 px обводку через plate/background token; - не является интерактивной целью и имеет `aria-hidden="true"`. Доступное имя/описание marker включает локализованную причину: - alarm: «Тревога»; - event: «Короткое событие»; - presence: «Обнаружено присутствие»; - transition: «Устройство перемещается/переключается»; - running: «Устройство работает». Визуальный эффект не должен быть единственным носителем критической информации. В рамках #98 обновляется существующее `aria-label` полного marker и `role="img"` preview; static card сохраняет read-only семантику. Добавление полной клавиатурной навигации по объектам плана, нового tab order или `role="button"` относится к #31 и не должно скрыто появиться в этой задаче. Для alarm кроме motion всегда остаются красная plate и локализованный текст в доступном имени/информационной поверхности. Для short/continuous отсутствие анимации при reduced motion не меняет действие по нажатию и hit area. ## 11. UI настроек устройства ### 11.1. Названия и подсказки Один общий `marker.display_hint` заменяется подсказкой для текущего mode. Нормативный смысл: - **Значок + состояние:** plate и icon показывают состояние без обычной пульсации; критическая тревога пульсирует красным. - **Значок + состояние и активность:** дополнительно показывает короткие события и непрерывную реальную активность. - **Значение + состояние:** выводит однозначное HA value, при невозможности — state-aware icon; alarm остаётся видимым. - **Всегда статичный значок:** marker не реагирует на HA state; hover, focus и click продолжают работать. Поля переименовать: - RU: «Цвет пульсации активности», «Размер пульсации активности»; - EN: `Activity pulse color`, `Activity pulse size`. Под полями: «Не влияет на красную тревогу» / `Does not affect red alarms`. Поля показываются только при `icon_ripple`. Их сохранённые значения не стираются при временном выборе другого display mode и применяются снова после возврата. ### 11.2. Preview `hp-device-preview` получает готовый `ResolvedDevicePulse`, а не вычисляет activity заново. В режиме `icon_ripple` доступны две независимые preview-команды: 1. **Показать короткую пульсацию** — запускает local-only short demo на 3,3 с. 2. **Показать постоянную пульсацию** — включает continuous demo до повторного нажатия, смены binding/display, появления real pulse или закрытия preview. Правила: - demo не меняет HA state, config и основной `_activityRt`; - real alarm/short/continuous имеет приоритет и отключает demo; - одновременно активен максимум один demo; - кнопка continuous меняет label на «Остановить постоянную пульсацию»; - reduced motion показывает dot и пояснение, а не кольцо; - preview facts различают reason, даже если rendered kind один; - fit учитывает максимальный ordinary pulse, alarm pulse и satellites без clipping. ## 12. Архитектура ### 12.1. Нормативный pipeline ```text HA states + registry → entityVisualSample(s) → combineVisualSamples → per-card finite edge runtime → resolveDevicePresentation → resolveDevicePulse → renderDeviceFace/device-pulse → interactive plan | preview | static card ``` Рекомендуется отдельный pure module `src/device-pulse.ts`: ```ts resolveDevicePulse({ display, visual, liveStates, effectiveHidden, bindingStatus, runtime, rippleColor, rippleScale, reducedMotion, }): ResolvedDevicePulse ``` Resolver не читает DOM, timers, `matchMedia` и global state. `now` передаётся явно через runtime/presentation options для детерминированных тестов. Полный `ResolvedDevicePresentation` может содержать pulse как вложенную проекцию, но не должен одновременно оставлять второй нормативный `activity + classes` path. На время одного beta допускается compatibility adapter старых классов; бизнес-решение всегда принимается до renderer. ### 12.2. Runtime ownership Finite runtime остаётся per card instance: - interactive card и static card не делят Map; - Map key — стабильный marker id; - отдельная `activitySourceSignature` включает binding и только visual/critical sources, которые способны породить semantic edge; - signature **не включает** display mode, value/value-badge source, LQI, temperature visibility, satellites, icon или preview-only presentation; - source signature change устанавливает новый baseline; - runtime хранит `startedAt`, `expiresAt`, semantic reason и generation; - один timeout только будит render около expiry; истина определяется `now < expiresAt`, а не фактом срабатывания timer; - disconnect очищает timers/Map; - hidden tab не проигрывает накопившийся short после возврата. Текущую `presentationSourceSignature()`, которая также включает value source, нельзя без проверки объявить activity signature: смена бейджа или глобального `show_temperature` не должна отменять либо искусственно перезапускать pulse. Допустимо сохранить старую функцию для memo/snapshot и добавить узкую `activitySourceSignature()` рядом с semantic resolver. `prefers-reduced-motion` подписывается один раз на surface/card host, а не на каждый marker. Host передаёт актуальный boolean в pure resolver и вызывает один re-render при изменении media query. Full card, static card и standalone preview самостоятельно владеют подпиской и очищают её при disconnect. Можно сохранить совместимость текущего `flashTs/flashKind/gen`, если наружный контракт даёт те же гарантии. Переименование runtime fields не является целью. ### 12.3. Один renderer `renderDeviceFace()` рендерит один слой: ```html ``` или continuous/alarm variant. Reduced-motion dot создаётся тем же renderer. Удалить: - `.dev.alarm::after` как отдельный pulse path; - статичную `.activity-ring.presence` ветку; - reduced-motion static ring; - surface-specific самостоятельное решение вида pulse. Класс `.dev.alarm` сохраняется только для alarm plate. Внешние `activity-event/presence/transition/running` classes можно оставить временно для compatibility hooks на один beta-cycle, но production CSS и новые тесты не должны от них зависеть. Перед stable их судьба фиксируется в реестре compatibility #33: удалить либо документировать как deprecated styling hooks. ## 13. Сохранение и обратная совместимость - `display` tokens не меняются; - `ripple` продолжает нормализоваться в `icon_ripple`; - `ripple_color` и `ripple_size` читаются/пишутся как раньше; - Open → Save без изменения display не материализует новые поля; - импорт/экспорт round-trip побайтно сохраняет эти поля; - backend validation limits не меняются; - downgrade видит прежние поля и может показать старый ring — это ожидаемая визуальная разница, не потеря данных; - schema/model migration не требуется. ## 14. Эдж-кейсы - несколько sources одновременно: alarm > newest short > continuous; - short приходит во время continuous и после себя возвращает актуальный continuous; - два short подряд перезапускают три волны через generation; - alarm возникает во время short и полностью его подавляет; - alarm снимается после expiry short — старый short не возвращается; - alarm onset очищает finite runtime и синхронизирует baseline всех sources; - binding/source graph изменился — старые edges не переносятся; - смена display, value badge, LQI и `show_temperature` не меняет activity signature и не создаёт edge; - `unavailable → on` после reconnect — baseline без события; - один unavailable sample не гасит доступный alarm/continuous другого sample; - быстрый `opening → open` прекращает continuous сразу; - прямой `closed → open` запускает short fallback; - custom color невалиден — безопасный fallback, без CSS injection; - `ripple_size` min/max, marker scale 0.5/3, value badge на четырёх сторонах; - hover/focus не меняет opacity/speed и не перезапускает animation; - `static_icon → icon_ripple` показывает только актуальную continuous семантику, но не старый finite event; - live_states on/off применяется немедленно без Save; - preview draft не мутирует runtime полного плана; - две карточки одного плана имеют независимый generation/timer; - reduced-motion меняется во время активного effect без remount; - media-query listener существует один раз на surface, а не на marker; - HA-disabled preview ghost не получает alarm/activity dot; - new-device dot и activity dot видны раздельно; - theme/contrast и пользовательский accent; - touch hover отсутствует, click target не увеличивается pulse layer. - background/resume просрочивает short по `expiresAt` до первого нового paint; замороженная браузером CSS-анимация не продолжает старые три волны. ## 15. Производительность и безопасность - один pure pulse resolve на уже построенную presentation; - не повторять `resolvedLightSources` ради цвета внутри каждого surface; - continuous смена HA state не создаёт новые DOM nodes каждый tick; - максимум один finite timeout на marker с short runtime; - CSS animation использует transform/opacity, не layout properties; - pulse layer `pointer-events: none`; - color проходит существующую safe color normalization; - нет HTML из HA state/labels; - golden/performance fixture не должен получить дополнительный full-plan rebuild при каждом animation frame. ## 16. Тестирование ### 16.1. Unit Обязательная таблица `semantic → pulse`: - все display modes × alarm/event/presence/transition/running/none; - alarm > short > continuous; - live_states false; - static_icon; - hidden, HA-disabled, orphaned/unavailable; - terminal fallback и замена real transition; - initial/reconnect/unavailable baseline; - rapid retrigger/generation; - source signature reset; - activity signature не зависит от display/value badge/LQI/temperature UI; - один unavailable source рядом с доступным alarm/running source; - alarm onset baseline reset и отсутствие stale short после alarm; - custom/fallback color и scale; - reduced-motion dot/none, hot media-query change, никогда static ring. ### 16.2. Browser smoke - motion, contact, button/event, presence, working switch, climate, cover, lock, valve, vacuum, script, alarm; - четыре display modes по полной матрице; - short три волны и cleanup после 3,3 с; - continuous появляется/исчезает по state; - hover не перезапускает timeline; - preview parity и обе demo-команды; - static card parity; - runtime независим между двумя card instances; - hide/resume после истёкшего short не продолжает замороженную CSS-анимацию; - reduced-motion dot geometry и absence of rings; - спутники marker не перекрываются. Таймеры тестируются управляемым `now`/fake clock; CI не ждёт реальные 3,3 с там, где достаточно pure assertion. ### 16.3. Golden Минимальная матрица: - desktop/mobile; - light/dark; - alarm; - short в фиксированном progress; - continuous; - reduced-motion dot; - четыре display modes; - presence без статичного кольца; - max ripple scale + value badge/LQI/new-device dot. Новые golden принимаются только из Linux CI artifact по HP-QA-01. ## 17. Документация и release-артефакты В той же реализации обновить: - `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #98; - `docs/ARCHITECTURE.md` — presentation/pulse pipeline; - `docs/LIGHT.md` — явно отделить marker pulse от Glow; - `docs/FILTERING.md` — semantic sources/lifecycle; - пользовательскую документацию по четырём display modes; - `docs/TESTING.md` — unit/smoke/golden матрицу; - ru/en i18n parity. Release body: значимое пользовательское изменение одной строкой + ссылки на RU/EN changelog. Поставка сначала в beta. Full golden/performance gate — перед stable release согласно действующей release policy. ## 18. План реализации 1. Ввести `activitySourceSignature`, pure `ResolvedDevicePulse` и исчерпывающие unit tests. 2. Подключить pulse к `ResolvedDevicePresentation` без изменения DOM и добавить одну reduced-motion subscription на surface. 3. Перевести `renderDeviceFace()` на единый `device-pulse` renderer. 4. Удалить alarm pseudo-element, presence static ring и reduced-motion ring. 5. Нормализовать finite runtime interactive/static cards. 6. Обновить display copy и activity settings. 7. Реализовать две preview demo-команды. 8. Обновить accessibility descriptions и satellite layout. 9. Добавить browser/golden coverage и документацию. 10. Выпустить beta, проверить реальные motion/presence/alarm/cover fixtures. ## 19. Критерии приёмки - [ ] В production есть один pure pulse resolver и один DOM renderer. - [ ] Визуальных pulse-kind ровно три: alarm, short, continuous. - [ ] Статичных колец нет в normal, reduced motion и preview. - [ ] Presence показывает continuous pulse, а не кольцо. - [ ] Real transition существует ровно пока идёт переход; terminal fallback — short около 3,3 с. - [ ] Alarm использует единый renderer, системный красный и имеет высший приоритет. - [ ] Матрица четырёх display modes соблюдается на всех surfaces. - [ ] `live_states: false` подавляет ordinary activity, но не alarm. - [ ] `static_icon` не показывает alarm/activity и сохраняет hover/focus/click. - [ ] Цвет/размер ordinary activity не влияют на alarm. - [ ] Reduced motion использует внутреннюю dot только для activity mode. - [ ] Preview показывает фактический результат и две безопасные demo-команды. - [ ] Первый snapshot/reconnect/unavailable recovery не создают false event. - [ ] Activity signature не зависит от value/satellite/display presentation. - [ ] Alarm onset очищает finite runtime; один unavailable source не подавляет доступную тревогу другого source. - [ ] Rapid retrigger не накапливает DOM/timers и корректно перезапускает цикл. - [ ] Background/resume не продолжает просроченную short-анимацию. - [ ] Модель, backend и существующие конфиги остаются совместимыми. - [ ] Full plan, preview и static card дают одинаковую projection. - [ ] ru/en, docs, unit, smoke и golden gates актуальны.