Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в `legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код, тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ — на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`), DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди перенесённых нет. Относительные ссылки перенесённых файлов переписаны (`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) — все 26 резолвятся. Попутно: битая ссылка в `089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` — теперь команда `git show` по истории. Строка в `legacy/README.md`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
11 KiB
#460 — Детерминированное завершение кадра живого редактора
Issue: #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-деревья — обязательные сопутствующие артефакты.
Контракт поведения
- Вызов внутренней точки синхронизации сначала дожидается уже ожидавшегося полного Lit-кадра, затем фиксирует последнюю запрошенную ревизию live-editor.
- Promise этой ревизии не завершается раньше фактической покраски DOM. Несколько изменений до одного RAF объединяются, но ожидание покрывает всю объединённую ревизию.
- Если полный Lit commit заменил промежуточный live-кадр, этот commit считается корректным завершением. Dispose отменяет RAF и завершает все ожидания.
- Присвоение
nullсвойствам hover-preview не маршрутизируется только в live-слой: полный Lit commit обязан убрать и live-, и settled-копию. - 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.