Files
houseplan-card/docs/specs/460-live-editor-settlement.md
T
2026-09-05 12:10:26 +03:00

171 lines
11 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.
# #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`.