diff --git a/docs/specs/461-wall-draw-click-performance.md b/docs/specs/461-wall-draw-click-performance.md new file mode 100644 index 00000000..ccf374b1 --- /dev/null +++ b/docs/specs/461-wall-draw-click-performance.md @@ -0,0 +1,473 @@ +# ТЗ #461 — быстрый commit промежуточной точки цепочки стен + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/461 +- **Статус:** первая редакция; канонический статус задаётся метками issue +- **Тип / приоритет:** bug / P1 +- **Область:** редактор плана, инструмент «Стены», physical-geometry и junction + preflight, wall-model write barrier, performance harness +- **Связано:** #451 (лёгкий live-слой и локальный preflight Resize), #330 + (линейные junction limits), #314 (атомарный откат отклонённой геометрии), + #294 (завершение цепочки без потери сегментов) + +## 1. Проблема + +В редакторе плана первый клик инструмента «Стены» только ставит начальную +точку. Каждый следующий клик синхронно сохраняет новый сегмент через общий +`_commitPhysicalGeometry()`. До того как браузер сможет показать принятый +отрезок и превью продолжения, этот общий барьер: + +1. проверяет физическую геометрию всего пространства; +2. канонизирует wall-segment model; +3. второй раз проверяет физическую геометрию всего пространства; +4. сравнивает junction limits по пространству; +5. записывает историю и запускает сохранение. + +На живом плане владельца — 12 комнат, 36 сегментов положительной толщины и +четыре сохранённых черновика — один такой клик занимает 1 278–1 309 мс. Около +720 мс уходят на две полные physical-geometry проверки, ещё около 273 мс — на +junction limits. При удалении четырёх черновиков одна полная проверка падает с +342 до 194 мс: активная и брошенные цепочки делают каждый следующий клик всё +дороже. + +Это отдельный hot path, не закрытый #451. #451 прекратила полный render на каждом +`pointermove`, но terminal click по-прежнему блокирует main thread общим +геометрическим commit. + +## 2. Сценарий и пользовательский результат + +**Персона:** администратор дома. + +**Поверхность:** desktop browser, редактор плана, инструмент «Стены»; безопасные +touch-ограничения редактора сохраняются без обещания полной touch-паритетности. + +**Момент:** пользователь последовательно ставит точки длинной цепочки в уже +нарисованном пространстве с комнатами, стенами и сохранёнными черновиками. + +**До:** после каждого клика со второй точки отрезок и превью продолжения +замирают примерно на секунду; задержка растёт вместе с планом и черновиками. + +**После:** каждый промежуточный отрезок появляется без заметной секундной +паузы, остаётся crash-safe и доступен через Undo; при завершении цепочки весь +план по-прежнему проходит полный fail-closed preflight. + +## 3. Scope + +### 3.1 Входит + +1. Отдельный bounded preflight/commit для сохранения очередного сегмента + активного `room_draft` в текущем model v9. +2. Локальная проекция предыдущего и нового состояния вокруг добавленного + сегмента с точной production-геометрией затронутого компонента. +3. Локальное сравнение новых junction-limit нарушений с переиспользованием + physical-geometry артефакта кандидата. +4. Безусловный полный preflight при завершении/преобразовании цепочки. +5. Fallback на существующий полный барьер для legacy-документа, который ещё не + материализован в текущую wall model. +6. Unit, production-bundle smoke, mutation witnesses и performance benchmark, + воспроизводящие размер и форму нагрузки из issue. +7. Документация технического write-контракта и release-артефакты. + +### 3.2 Не входит + +- показ сегмента до окончания проверки, асинхронный optimistic commit и откат + уже показанного кадра; +- изменение snap, hover, толщины, автообъединения, создания комнаты, Esc, + Ctrl/Cmd+Z, смены инструмента или поведения `pointercancel`; +- автоматическое удаление или слияние существующих брошенных черновиков; +- ослабление правил physical geometry или junction limits; +- ускорение остальных generic-вызовов `_commitPhysicalGeometry()`; +- изменение backend validation, WebSocket protocol, schema/model version, + импорта, экспорта или Optimize; +- новая настройка, Labs-флаг или пользовательский текст. + +## 4. Нормативный UX-контракт + +### 4.1 Первый и последующие клики + +- Первый чистый клик при пустой цепочке по-прежнему ставит только начальную + точку и не создаёт config write. +- Каждый принятый клик начиная со второго добавляет ровно одну точку и один + segment record с выбранной толщиной. +- Обработчик не возвращает управление до локального verdict: новый сегмент не + показывается как принятый, если write barrier его отклонил. +- После успеха SVG-превью начинается в новой последней точке; выбранный + инструмент и active draft остаются теми же. +- Повторный клик, невалидная толщина, превышение лимитов, pan/pinch, + `pointercancel` и второй pointer сохраняют действующие no-extra-segment и + no-extra-write правила. + +### 4.2 История и persistence + +- Один успешный промежуточный клик остаётся одним шагом истории с именем + `history.draft_segment`. +- Undo/Redo удаляет и возвращает именно последнюю принятую точку вместе с её + толщиной и стабильным segment ID. +- Сохраняется полный config, а не локальный patch. Очередь записи, revision, + pending physical transaction и server rejection rollback следуют контракту + #314 без отдельного пути. +- Отказ preflight до записи восстанавливает `before` локально, очищает + недопустимый gesture тем же способом и показывает существующий toast. Он не + оставляет точку, укороченный массив толщин, history entry или config write. +- Отказ backend после optimistic write восстанавливает самый ранний snapshot + pending-транзакции; более поздний отклонённый сегмент не может быть + легализован следующим кликом. + +### 4.3 Завершение цепочки + +Любой действующий terminal path — смена инструмента/редактора/пространства, +`Esc` по #294 и штатное создание/преобразование комнаты — сохраняет свою +нынешнюю семантику. Перед финальным принятием результата вызывается generic +full-space `_commitPhysicalGeometry()`: + +- полный physical-geometry preflight не заменяется локальным результатом и не + берётся из его кэша; +- junction limits сравниваются для полного пространства; +- полный отказ не оставляет частично преобразованную комнату/partition; +- успешное завершение по-прежнему удаляет active draft и сохраняет ordinary + partitions или комнату в соответствии с текущим сценарием. + +Задержка одного terminal действия допустима; переносить её обратно на каждый +промежуточный клик запрещено. + +## 5. Граница fast path + +Fast path выбирается только когда одновременно выполняются условия: + +1. вызов сделан из `_persistActiveDraftSegment()` после добавления одной точки; +2. `before` и live config относятся к одному существующему пространству; +3. документ уже имеет текущую `WALL_SEGMENT_MODEL_VERSION`; +4. изменилось только содержимое одного active `room_draft`, а прочая физическая + геометрия пространства byte-equivalent соответствующему `before`; +5. новая запись имеет конечные координаты, согласованные `points/segments` и + стабильную идентичность. + +Если любое условие не доказано, используется существующий generic full-space +barrier. Неизвестное состояние не считается разрешением ускоренного пути. + +Первый структурный write legacy-документа обязан пройти прежнюю полную +materialisation/identity проверку. Если он успешен и документ стал текущей +версии, следующие сегменты той же цепочки могут использовать fast path. + +## 6. Локальный physical-geometry proof + +### 6.1 Seed + +Seed — новый segment active draft и две его конечные точки. Предыдущие сегменты +этой цепочки не включаются автоматически целиком: в proof входят только те, +которые геометрически способны взаимодействовать с seed по правилам ниже. +Именно это не даёт стоимости расти с каждым кликом длинной цепочки. + +### 6.2 Что обязательно входит + +Одинаковый pure projector строит local `before` и local `candidate`. Он обязан +сохранить идентичность и неизвестные поля выбранных записей и включить: + +- seed и все положительно-толстые/нулевые rays, инцидентные любой его конечной + точке; +- коллинеарный связный run, необходимый для правила минимальной длины стены; +- room wall atoms и комнаты, чьи реальные wall/body envelopes касаются seed + либо первого слоя включённых соседей; +- partitions и сегменты других drafts, тело или ось которых пересекает либо + входит в применимый 5-сантиметровый clearance seed-компонента; +- wall columns, способные пересечь тот же компонент с учётом фактического + размера; +- openings/open spans, hosted либо геометрически относящиеся к выбранным + стенам/комнатам/partitions; +- все rays конкретного junction, если в компонент вошёл хотя бы один его ray, + чтобы angle/valence нельзя было доказать неполным узлом; +- malformed/непозиционируемую физическую запись, если projector не может + доказать, что она удалённая. Неизвестное fail-closed, а не исчезает из proof. + +Простое сравнение AABB осей без толщины недопустимо. Расстояния учитывают +фактическую половину толщины обеих стен/перегородок, размер колонны, grid +epsilon и 5-сантиметровое правило узлов. + +### 6.3 Что исключается + +Комнаты, стены, независимые тела и старые drafts, для которых pure projector +доказал отсутствие указанного контакта, не входят в local config. Удвоение +числа таких удалённых объектов не должно удваивать стоимость клика. + +Фильтрация не изменяет исходный config и не становится новой persist-моделью. +Она создаёт только runtime proof input. + +### 6.4 Exact production pass + +На local candidate выполняется `checkSpacePhysicalGeometry()` с теми же +`prepareSpacePhysicalGeometryInputs`, `wallBodiesGeometry` и +`floorFootprintGeometry`, что используются generic barrier/Optimize. Упрощённая +геометрическая формула специально для теста или fast path запрещена. + +Успешный wall pass отдаёт свой `roomGeom`/`multiWallNodes` как transient +артефакт следующему junction proof. Один candidate click не оплачивает тот же +wall union второй раз. + +Исключение, `null`, `degraded-extra`, `failed-core`, floor failure либо +невозможность построить local candidate отклоняют fast path и приводят к +generic full barrier; они не дают fail-open принятия. + +## 7. Локальный junction-limit proof + +- Previous и candidate сравниваются после одной и той же wall-model + materialisation и через один и тот же local projector. +- `checkNodes`, valence/angle, полная длина включённого collinear run и + node-distance применяются ко всем segment records local component. +- Room-clearance считается только для вошедших комнат и использует wall + artifact из §6.4; повторный union запрещён. +- Унаследованное local-нарушение не блокирует клик. Любое увеличение нарушения + по действующим правилам отклоняет его тем же toast, что generic barrier. +- Ошибка candidate judgment остаётся `check_failed` и отклоняет write. +- Кэш baseline допустим только по identity/fingerprint `before`, space и seed + component; stale reuse после следующего принятого сегмента запрещён. + +## 8. Эквивалентность generic barrier + +На наборе локальных вмешательств fast path и generic full barrier обязаны иметь +одинаковый verdict: + +- новый сегмент продолжает свободную цепочку; +- 14°/15° junction; +- valence 6/7; +- стена 19/20 см и короткий атом внутри более длинного collinear run; +- node distance 4/5 см до чужой стены и узла; +- пересечение комнаты, partition, другого draft и column; +- физический `wall-degraded-extra` на соседней комнате из production fixture + #278; +- hosted room/partition opening рядом с seed; +- нулевая стена; +- malformed local carrier. + +Отдельная fixture добавляет много удалённых валидных комнат и drafts. Их +наличие не меняет verdict локального вмешательства. Full terminal barrier всё +равно видит намеренно повреждённую удалённую геометрию и отказывает завершению. + +## 9. Performance-контракт + +### 9.1 Профиль + +Добавляется отдельный воспроизводимый профиль `wall-draw-click-v1` на +production bundle. Основная fixture соответствует живому измерению: + +- пять пространств в config; +- редактируемое пространство: 12 комнат, не менее 36 сегментов с + положительной толщиной и четыре сохранённых open drafts; +- активная новая цепочка ставит не менее семи последовательных сегментов после + warm-up; +- remote-вариант удваивает число не взаимодействующих с seed физических + сегментов без изменения local component. + +Runner измеряет время terminal click от входа в production handler до его +возврата, число full/local physical passes, wall unions, junction passes, +history entries и config writes. Synthetic click считается валидным только +если он действительно дошёл до `_persistActiveDraftSegment()` и изменил draft. + +### 9.2 Структурные assertions + +Для каждого промежуточного клика текущего v9-документа: + +- `fullSpacePhysicalChecks = 0`; +- `localPhysicalChecks = 1`; +- candidate wall union выполнен не более одного раза и переиспользован в + junction proof; +- одна history entry и одна queued config write; +- число объектов local proof ограничено геометрическим компонентом, а не + общим числом комнат/черновиков пространства. + +Для terminal finish — как минимум одна независимая full-space physical +проверка и полный junction verdict. Для legacy первого structural write — +generic full barrier, а не fast path. + +Assertions краснят runner независимо от времени. + +### 9.3 Абсолютные и scaling budgets + +На каноническом Linux browser runner после warm-up: + +| Метрика | Бюджет | +|---|---:| +| median terminal click, fixture 36 segments + 4 drafts | ≤ 150 мс | +| худший из 7 измеряемых кликов | ≤ 250 мс | +| remote-вариант с удвоенным числом удалённых сегментов | не более `base × 1.5 + 20 мс` | + +Это bootstrap-потолки относительно измеренных 1 278–1 309 мс, а не разрешение +расходовать весь кадр. Они дополняют structural assertions и существующие +base-relative performance checks. Ослаблять другие budgets запрещено. + +Node/unit benchmark дополнительно проверяет, что 80 удалённых сегментов не +дороже 40 более чем вдвое; его главный witness — счётчики выбранного local +component, поэтому шум таймера не может скрыть возврат full-space пути. + +## 10. Acceptance criteria + +- **AC1 (`unit` + `smoke`):** первый клик не пишет config; каждый следующий + успешный клик добавляет одну точку/толщину/ID, одну history entry и одну + queued write, оставляя active draft и preview в новой точке. +- **AC2 (`unit` + code review):** fast path доступен только + `_persistActiveDraftSegment()` на доказанном current-model draft-only diff; + legacy, mixed либо unknown diff использует generic barrier. +- **AC3 (`unit`):** pure local projector выполняет правила §6 для комнат, + room walls, partitions, drafts, columns, openings, zero walls, junction rays + и malformed records; удалённая геометрия исключается детерминированно. +- **AC4 (`unit` + mutation witness):** verdict fast/full совпадает на матрице + §8. Мутации «выбросить соседнюю комнату», «не включать чужой draft/column» и + «не замыкать junction rays» делают соответствующие проверки красными. +- **AC5 (`unit` + mutation witness):** candidate physical artifact + переиспользуется junction proof и candidate wall union не считается второй + раз; возврат повторного union красит счётчик/benchmark. +- **AC6 (`unit` + `smoke`):** отказ local proof полностью восстанавливает + `before`, не пишет history/config и показывает действующий rule/geometry + toast; снятие rollback красит отрицательный smoke. +- **AC7 (`smoke` + mutation witness):** все terminal paths §4.3 выполняют + независимый full-space preflight. Мутация, заменяющая его local verdict, + краснеет на fixture с удалённой повреждённой геометрией. +- **AC8 (`smoke`):** Undo/Redo, server rejection rollback #314, merge двух + drafts, room offer/creation, Esc #294, смена инструмента и pointer/touch + cancellation сохраняют существующие результаты и число записей. +- **AC9 (`performance`):** профиль `wall-draw-click-v1` проходит structural, + absolute и scaling budgets §9; возврат generic full preflight на каждый клик + доказанно красит профиль. +- **AC10 (`typecheck` + `unit` + `build`):** implementation-loop гейты зелёные, + no-new-any не ухудшен, generated bundles синхронны. +- **AC11 (`golden` + existing smoke):** задача не меняет пиксели; canonical + light/dark Plan frames совпадают с эталоном, существующие wall-draw, + wall-chain, physical-geometry, junction и touch smokes зелёные без ослабления + ожиданий. +- **AC12 (`documentation review`):** architecture/compatibility и performance + README описывают новую границу; оба changelog содержат пользовательский + эффект со ссылкой на #461. Новых i18n-ключей нет. + +## 11. План автотестов + +### 11.1 Unit + +- projector: выбор каждого типа локальной геометрии, фактическая толщина и + clearance, incident closure, collinear run, malformed fail-closed, stable + identity/unknown fields, отсутствие mutation исходного config; +- route guard: v9 draft-only, legacy, mixed diff, no-op и missing snapshot; +- parity матрица §8 для previous/candidate; +- wall-artifact reuse и число full/local passes; +- history/pending-write/rejection invariants. + +### 11.2 Targeted production-bundle smoke + +- реальная pointer/click цепочка на fixture §9: preview, persistence, Undo/Redo, + Esc/finish и повторный вход; +- local physical/junction rejection с проверкой UI и нулевой записи; +- backend rejection после двух быстро принятых сегментов; +- mouse и один безопасный touch путь: pan/pinch/cancel не создают сегмент; +- все сценарии synthetic events обязаны подтвердить вход в production handler. + +### 11.3 Mutation gate + +В `scripts/mutation-gate.mjs` регистрируются как минимум witnesses: + +1. `wall-draw-full-preflight-again` — промежуточный click возвращён на generic + full-space barrier; +2. `wall-draw-local-neighbour-dropped` — projector теряет взаимодействующий + соседний body/room; +3. `wall-draw-terminal-full-check-skipped` — finish доверяет local verdict; +4. `wall-draw-rejection-rollback-skipped` — отклонённая точка остаётся в draft. + +Каждый мутант связан с точной командой и ожидаемым красным тестом для таблицы +«AC · чем доказан · чем краснеет» код-ревью. + +### 11.4 Гейты + +- цикл реализации: `npm run typecheck`, `npm test`, `npm run build`; +- до передачи на код-ревью: targeted production-bundle smoke, benchmark #461, + `npm run bundle:sync`, `npm run bundle:budget`, `check-docs`, `no-new-any`; +- canonical screenshots после `src/**`: только Linux capture, ожидается нулевой + diff; принимать новые эталоны этой задачей нельзя без найденной отдельной + визуальной причины; +- перед бетой: полный smoke/golden/performance и HA harness по runbook. + +## 12. Модель данных, совместимость и i18n + +- Schema и `model_version` не меняются. +- Persisted `rooms`, `wall_segments`, `room_drafts[].points/segments`, + partitions, columns и openings сохраняют существующий формат и unknown-field + round-trip. +- Legacy read остаётся side-effect free; первый structural write проходит + полный current barrier и материализует wall identity по прежним правилам. +- Older frontend/backend interoperability не меняется: сервер получает тот же + полный config и применяет свои действующие validators. +- Новых i18n-ключей и текстов нет; используются существующие history/toast + строки. + +## 13. Touch, accessibility и security + +- View, kiosk и household-member surfaces не затронуты. +- Редактор остаётся desktop-first. Один чистый touch tap может добавить один + сегмент; pan, pinch, second pointer и `pointercancel` — ни одного, как требует + `docs/TOUCH-SUPPORT.md`. +- Keyboard, focus и screen-reader контракт не меняются. +- Новых HA actions, прав, network endpoints и данных в логах нет. +- Проектор/benchmark не публикует пользовательскую геометрию; fixture + синтетическая. + +## 14. Ожидаемые модули и файлы + +Точные имена новых pure-модулей не являются продуктовым контрактом. Ожидаются: + +1. `src/houseplan-editor-runtime.ts` — выбор fast path, commit/rollback/history + и terminal full barrier. +2. Новый узкий `src/*draft*preflight*.ts` либо обобщённый существующий helper — + pure local projector и proof result/counters. +3. При необходимости `src/resize-live-preflight.ts`, + `src/plan-geometry-preflight.ts`, `src/junction-limits.ts` — только + переиспользуемые seams без изменения действующих verdicts. +4. `test/*draft*preflight*.test.mjs` и регрессии существующих wall/junction + suites. +5. `demo/smoke_*wall*draw*.mjs`, `demo/benchmark_*wall*draw*.mjs`, performance + budgets/README и регистрация smoke/CI. +6. `scripts/mutation-gate.mjs` и fixtures witnesses. +7. `docs/ARCHITECTURE.md`, `docs/CONFIG-COMPATIBILITY.md`, + `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`; generated bundles. + +## 15. Риски и меры + +| Риск | Мера | +|---|---| +| Local projector не включает влияющего соседа | §6 задаёт физические envelopes и closure; parity fixtures и три отрицательных мутанта; terminal full check независим | +| Ускорение случайно ослабляет legacy migration | fast path закрыт до current model; отдельный legacy regression | +| Старое нарушение удалённой области блокирует локальный клик иначе, чем раньше | это намеренная граница: локальный клик отвечает только за введённое им изменение; terminal full check сохраняет общий verdict | +| Повторное использование wall artifact становится stale | artifact живёт один click и связан с candidate fingerprint; тест меняет candidate между вызовами | +| Performance test флапает | structural counters являются главным verdict; timing — median после warm-up с широким bootstrap budget | +| Backend позже отклоняет промежуточную запись | прежняя очередь и атомарный rollback #314 не обходятся; end-to-end rejection smoke | +| Рефакторинг generic barrier затрагивает другие редакторские операции | fast path доступен только одному call site; все остальные вызовы остаются прежними | + +## 16. Откат + +Чистый revert fast path возвращает `_persistActiveDraftSegment()` на generic +barrier. Миграция и восстановление данных не нужны: ускоренный путь сохраняет +тот же current-model config. Новые runtime helpers/caches не переживают remount. + +Если performance-гейт выявит недостаточную локализацию, откатывается fast path, +а не ослабляются physical/junction правила, rollback либо terminal full check. + +## 17. Release-артефакты + +- обычная следующая beta после зелёного code review; +- `docs/CHANGELOG.ru.md` и `docs/CHANGELOG.md`: одна пользовательская запись о + быстрой фиксации отрезков на больших планах со ссылкой на #461; +- `docs/ARCHITECTURE.md` и `docs/CONFIG-COMPATIBILITY.md`: intermediate-local / + terminal-full write boundary; +- `demo/performance/README.md`: профиль, counters, budgets и локальный запуск; +- пользовательский гайд не меняется: видимое поведение и команды прежние; +- новые screenshots/golden не ожидаются; canonical capture должен подтвердить + отсутствие pixel diff; +- release body формируется штатно при выпуске и не обещает численный budget + конкретного компьютера пользователя. + +## 18. Принятые предположения + +1. Не показываем отрезок до verdict и не вводим асинхронный optimistic кадр: + пользователь видит прежнюю последовательность, только быстрее. +2. Не удаляем брошенные drafts автоматически — это пользовательские данные; + задача лишь перестаёт платить их полную стоимость на каждом новом клике. +3. Не ускоряем generic `_commitPhysicalGeometry()` для resize, openings, + columns и других writers: их оптимизация требует отдельных профилей. +4. Число 150 мс — acceptance budget канонического runner, а не обещание + одинакового wall-clock времени на любом клиентском устройстве. +5. Точные имена helper-файлов и форма bounded runtime cache технические и могут + меняться при ревью без изменения UX/AC. diff --git a/docs/specs/README.md b/docs/specs/README.md index a3a8f19a..0c53960a 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -181,6 +181,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#456](https://github.com/Matysh/houseplan-card/issues/456) Копирование пространства без комнат и устройств | [456-copy-space.md](456-copy-space.md) | | [#457](https://github.com/Matysh/houseplan-card/issues/457) Направление Zigbee-связей к координатору | [457-zigbee-route-arrows.md](457-zigbee-route-arrows.md) | | [#460](https://github.com/Matysh/houseplan-card/issues/460) Детерминированное завершение кадра живого редактора | [460-live-editor-settlement.md](460-live-editor-settlement.md) | +| [#461](https://github.com/Matysh/houseplan-card/issues/461) Быстрый commit промежуточной точки цепочки стен | [461-wall-draw-click-performance.md](461-wall-draw-click-performance.md) | ## P3