# #460 — Детерминированное завершение кадра живого редактора Issue: [#460](https://github.com/Matysh/houseplan-card/issues/460) ## Сценарий Администратор дома размещает мебель мышью в редакторе подложки. Браузерные проверки воспроизводят тот же путь: входят в редактор, двигают указатель, сравнивают preview с сохранённой фигурой и убирают указатель с плана. ## Что человек увидит до и после До исправления preview мебели после ухода указателя изредка остаётся на плане; после исправления он всегда исчезает, а внешний вид и результат обычного размещения мебели не меняются. ## Проблема После #451 pointer-owned состояние редактора может отрисовываться отдельным RAF вне цикла Lit. `updateComplete` больше не доказывает, что этот кадр уже применён, а локальная эвристика из двух RAF в browser smoke иногда снимает старую физическую толщину preview. На чистом `dev` мебельный smoke упал 7 раз из 12. Диагностика выявила ещё две части одного сценария: - программный smoke мог начать жесты после завершения mode transition, но до завершения вызванного им refit viewport, поэтому сравнивал разные камеры; - terminal `null` для hover-состояния шёл через облегчённый live-маршрут. Если полный Lit-кадр до этого успел нарисовать preview в основном слое, очистка live-слоя оставляла этот settled preview видимым. ## Скоуп - Внутренняя awaitable-точка завершения последнего уже запрошенного кадра `live-editor`. - Корректное завершение ожиданий при coalescing, полном Lit commit и dispose. - Полный Lit commit для terminal-null hover-состояния. - Переход трёх затронутых browser smoke с фиксированных RAF на внутренний контракт; мебельный smoke начинает жесты только после наблюдаемого завершения mode transition и refit viewport. - Детерминированные unit-, browser- и mutation-доказательства. ## Не-скоуп - Изменение геометрии, магнита, размеров, толщины или opacity мебели. - Новый публичный API карточки либо Home Assistant. - Переписывание `live-viewport`, mode transition или общей архитектуры #451. - Новое поведение touch/pen, View или kiosk. - Изменение формата конфигурации, сохранённых данных или истории Undo/Redo. ## Затронутые файлы и модули - `src/live-editor.ts` — ревизии, ожидания и terminal-null routing. - `src/houseplan-editor-runtime.ts` — внутренний awaitable-метод для browser harness. - `test/live-editor.test.mjs`, `scripts/mutation-gate.mjs` и `scripts/smoke-links.mjs` — unit-, mutation- и smoke-selection доказательства. - `demo/smoke_furniture.mjs`, `demo/smoke_decor.mjs`, `demo/smoke_decor_text.mjs` — потребители контракта. - `docs/DEVELOPMENT.md`, оба changelog и сгенерированные bundle-деревья — обязательные сопутствующие артефакты. ## Контракт поведения 1. Вызов внутренней точки синхронизации сначала дожидается уже ожидавшегося полного Lit-кадра, затем фиксирует последнюю запрошенную ревизию live-editor. 2. Promise этой ревизии не завершается раньше фактической покраски DOM. Несколько изменений до одного RAF объединяются, но ожидание покрывает всю объединённую ревизию. 3. Если полный Lit commit заменил промежуточный live-кадр, этот commit считается корректным завершением. Dispose отменяет RAF и завершает все ожидания. 4. Присвоение `null` свойствам hover-preview не маршрутизируется только в live-слой: полный Lit commit обязан убрать и live-, и settled-копию. 5. Browser smoke не определяет готовность через `setTimeout`, sleep либо фиксированное число RAF. Отдельная подготовка мебельного сценария ждёт фактическое состояние mode/refit; RAF используется только как цикл доставки браузерного состояния с проверяемым условием выхода. ## UX Новых контролов и текстов нет. Предпросмотр остаётся тем же полупрозрачным символом и очищается по уже документированным границам из `docs/FURNITURE.md`: pointer leave, Escape, смена палитры, инструмента, редактора, пространства и remount. ## Модель данных и миграция Модель данных не меняется. Ревизии и ожидающие callbacks существуют только в памяти экземпляра live-editor и уничтожаются вместе с ним. Миграция не нужна. ## i18n Новых строк интерфейса нет. Словари не меняются. ## Критерии приёмки ### AC1 — явная синхронизация Управляемый unit-тест доказывает, что Promise не завершается до live paint, покрывает coalesced-изменения и не зависает после полного commit или dispose. ### AC2 — очистка terminal hover Unit-тест доказывает, что terminal-null hover требует полного Lit commit, а `demo/smoke_furniture.mjs` после `pointerleave` не находит preview ни в live-, ни в settled-слое. ### AC3 — browser-путь без временной лотереи `smoke_furniture.mjs`, `smoke_decor.mjs` и `smoke_decor_text.mjs` используют внутренний awaitable-контракт вместо локального двойного RAF. Мебельный smoke проходит не менее 10 последовательных запусков без падений; mode/refit завершён до первого программного жеста. ### AC4 — mutation-защита Мутант, который разрешает Promise до live paint, и мутант, возвращающий мебельному smoke двойной RAF вместо контракта, детерминированно ловятся своими гейтами. ## План автотестов - `test/live-editor.test.mjs`: fake RAF, coalescing, terminal-null, commit и dispose; статический контракт трёх smoke. - `demo/smoke_furniture.mjs`: preview/commit, pointer leave и минимум 10 последовательных полных запусков. - `demo/smoke_decor.mjs`, `demo/smoke_decor_text.mjs`: по одному полному целевому запуску после замены ожидания. - `scripts/mutation-gate.mjs`: два адресных мутанта из AC4. - Обычные `typecheck`, unit, build, bundle sync/budget, `no-new-any` и `check-docs` перед код-ревью. ## Риски - Потерянная ревизия могла бы завершить Promise слишком рано либо оставить его навсегда ожидающим; fake-RAF тест отдельно проверяет обе стороны. - Полный commit и dispose могут отменить RAF, поэтому они обязаны завершать ту же ревизию, а не просто очищать callback браузера. - Подготовка smoke не должна превращаться в ещё одну временную эвристику: условие выхода связано с mode/refit state, а не с числом кадров. - Terminal-null переводится в более дорогой полный render только на границе hover-сессии, не на каждом pointermove; горячий путь #451 сохраняется. ## Откат Один revert возвращает прежнюю маршрутизацию и smoke-ожидания. Конфигурация и пользовательские данные не требуют обратного преобразования. ## Release-артефакты - Одновременные записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`. - `docs/FURNITURE.md` уже содержит правильный пользовательский контракт очистки preview и не требует изменения. - `docs/DEVELOPMENT.md` фиксирует, что `updateComplete` не является барьером live-editor и browser harness обязан использовать явный settlement contract. - Стабильный screenshot/golden не меняется: исправляется transient-состояние, которого после завершения жеста не должно существовать. - Любое изменение `src/**` требует актуального Linux-артефакта `Docs screenshots` и приёмки его fingerprint по общему процессу. ## Принятые технические предположения — можно менять на ревью - Контракт остаётся методом lazy editor runtime и не становится API карточки. - Монотонная ревизия и список ожидающих Promise принадлежат `live-editor`, где создаётся и завершается RAF. - Синхронизация mode/refit остаётся частью setup мебельного smoke; отдельный публичный settlement API для viewport в этой задаче не вводится. - Изменение считается пользовательски видимым из-за исправления stale preview, поэтому коммит получает `User-Visible: yes`.