From 3a68efa62f5d37cd2f456c331d4113d0d1dbf9ce Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Wed, 19 Aug 2026 20:09:55 +0300 Subject: [PATCH] docs: specify optimizer micro-interval cleanup Issue: #198 User-Visible: no --- docs/specs/198-optimize-micro-interval.md | 197 ++++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 198 insertions(+) create mode 100644 docs/specs/198-optimize-micro-interval.md diff --git a/docs/specs/198-optimize-micro-interval.md b/docs/specs/198-optimize-micro-interval.md new file mode 100644 index 00000000..f9bc4976 --- /dev/null +++ b/docs/specs/198-optimize-micro-interval.md @@ -0,0 +1,197 @@ +# Issue #198 — очистка изолированного микро-интервала толщины через Optimize + +- Дата: 2026-08-19 +- Тип: bug / maintenance canonicalisation · приоритет P3 +- Оценка: пользовательская ценность 3/10 · ценность для разработки 5/10 · сложность 5/10 · риск 7/10 +- Issue: [#198](https://github.com/Matysh/houseplan-card/issues/198) +- Ветка: `issue/198-optimize-micro-interval` + +Канонические документы: `docs/SCOPE.md`, `docs/WALL-THICKNESS.md`, +`docs/USER-GUIDE.ru.md`, `docs/TESTING.md`. + +## 1. Сценарий и персона + +Администратор обслуживает старый или импортированный план через явное действие +«Общие настройки → Оптимизировать планы». В профиле одной сплошной стены между +двумя участками одинаковой толщины сохранился почти недоступный редактору +островок другой толщины короче половины шага сетки. Администратор ожидает, что +предпросмотр Optimize предложит безопасно убрать этот артефакт, а подтверждение +оставит один ровный участок с рабочей серверной отменой. + +## 2. Что человек увидит до и после + +**До:** профиль `22 → 15 → 22 см` с центральным участком около 0,7 см считается +каноническим. Повторный Optimize сообщает `changed: false`; на большом масштабе +может быть видна ступень, а лишняя точная граница усложняет downstream boolean- +геометрию. + +**После:** только явный Optimize распознаёт такой строго изолированный островок, +перекрашивает его в толщину одинаковых соседей и канонизирует три записи в один +максимальный пролёт 22 см. Изменение видно в существующем preview/report, +записывается лишь после подтверждения и отменяется существующим Undo Optimize. + +## 3. Подтверждённая причина + +`normalizeWallIntervals()` намеренно lossless: точные `WallEntry.a/b` являются +продуктовой границей толщин, поэтому helper сохраняет положительный интервал +любой длины. `degradeWalls()` также не вводит минимальную длину. Это правильный +контракт для редактора и рендера, но явному maintenance-действию не хватает +узкого lossy-правила. + +На fixture из #197 запись 15 см длиной `0.0013819039` нормализованной координаты +(около трети `GRID_STEP_N`) находится между соприкасающимися 22-см записями на +одной линии. Удаление только этой записи позволяет следующей канонизации собрать +единый 22-см пролёт. Optimize не создаёт островок из равномерного пролёта; +`rekeyWallsAfterMove()` может лишь перенести уже существующую точную границу при +выравнивании старой геометрии. + +## 4. Scope + +- добавить optimizer-only очистку эффективного профиля стены; +- схлопывать центральный интервал только при всех защитных условиях §6; +- выполнять очистку после rekey/выравнивания текущей геометрии и до финальной + lossless-нормализации wall entries; +- отражать результат через действующие `canonicalized` и `wallsMerged`, без + нового счётчика и UI-строк; +- сохранить preview/apply/server Undo, идемпотентность и compatibility; +- добавить unit, targeted browser smoke и mutation guard. + +## 5. Non-scope + +- универсальная минимальная длина стены или удаление любого короткого участка; +- фоновая очистка при чтении, рендере, обычном Save или редактировании; +- изменение lossless-контрактов `normalizeWallIntervals()`, `degradeWalls()`, + `wallIntervals()` и persisted schema; +- очистка короткого интервала на торце стены, у opening/virtual span, у вершины + комнаты либо между соседями разной толщины; +- исправление #197/#199/#201, изменение геометрии комнат, snapping или default + толщины; +- эвристики по большинству, максимальной толщине или физической толщине стены. + +## 6. Контракт безопасного схлопывания + +Центральный положительный effective interval может получить толщину соседей, +только если одновременно выполнены условия: + +1. его геометрическая длина строго меньше `0.5 × GRID_PITCH` в render scale + (эквивалентно `0.5 × GRID_STEP_N` в persisted scale); +2. непосредственно слева и справа существуют положительные solid intervals; +3. оба соседа коллинеарны центру, соприкасаются с его endpoints в действующем + scale-relative tolerance и имеют одну одинаковую толщину; +4. центральная толщина отличается от толщины соседей; +5. ни один endpoint центра не является вершиной room polygon, концом + `open_span`/resolved open cut или иным топологическим разрывом текущего + effective wall profile; +6. все три interval принадлежат одной физической линии; параллельные стены и + совпадения лишь по midpoint не объединяются. + +Ровно `0.5 × pitch` сохраняется. Цепочка из двух или более соседних коротких +интервалов другой толщины не схлопывается за один или несколько проходов: +кандидаты определяются по исходному effective profile текущего Optimize, а не +каскадно по уже изменённому результату. Если один interval одновременно +участвует в нескольких неоднозначных кандидатах, профиль сохраняется. + +После одновременного применения доказанных замен действующая +`normalizeWallIntervals()` собирает максимальные равные пролёты. Сам helper +должен быть чистым, детерминированным и не менять входные config/arrays. + +## 7. Preview, запись, Undo и данные + +Первый `optimizePlans()` возвращает `changed: true`, увеличивает +`canonicalized` для затронутого пространства и уменьшает число wall fragments, +что уже отражается в `wallsMerged`. Предпросмотр не мутирует входной config. +Повторный вызов на результате возвращает `changed: false` и нулевые новые +изменения. + +Запись происходит существующим подтверждением Optimize. Серверная резервная +копия и её срок жизни не меняются; Undo возвращает исходные три exact entries. +Обычное открытие старого плана остаётся lossless. Model version и schema не +меняются: это явно запрошенная канонизация данных, а не автоматическая миграция. +Downgrade читает получившийся единый валидный wall entry. + +## 8. UX, i18n, accessibility и touch + +Новых controls, строк, focus/keyboard/touch semantics и locale keys нет. +Используются существующие admin-only preview, подтверждение и Undo. Изменение +видно в уже локализованном количестве объединённых фрагментов стен; отдельное +предупреждение не добавляется. + +## 9. Acceptance criteria + +| AC | Критерий | Доказательство | +|---|---|---| +| AC1 | Fixture `22 → 15 → 22`, центр `< 0.5 pitch`, без узлов/cuts становится одним 22-см entry. | Focused `plan-optimizer` unit. | +| AC2 | Порог строгий: центр `= 0.5 pitch` и `> 0.5 pitch` сохраняется. | Boundary unit matrix в normalized и render scale. | +| AC3 | Центр на торце, соседи разной толщины, цепочка микро-интервалов, offset/perpendicular соседи и неоднозначное перекрытие сохраняются. | Negative unit matrix. | +| AC4 | Room vertex или endpoint opening/open span на любой границе центра запрещает очистку. | Geometry/open-cut units. | +| AC5 | Направление endpoints и перестановка wall/room records не меняют результат; входы не мутируют. | Reversal/permutation/immutability units. | +| AC6 | Preview сообщает изменение через `changed`, `canonicalized` и `wallsMerged`; второй запуск идемпотентен. | `plan-optimizer.test.mjs`. | +| AC7 | UI Optimize показывает preview, Apply сохраняет ровный профиль, Undo возвращает исходный; Cancel ничего не пишет. | Targeted production-bundle browser smoke. | +| AC8 | Lossless editor/render helpers без вызова Optimize сохраняют тот же микро-интервал. | Existing + focused regression unit. | +| AC9 | Regression защищена мутантом, отключающим optimizer-only cleanup. | `mutation-gate --check` + focused guard. | +| AC10 | Рабочие gates зелёные. | typecheck, unit, build, targeted smoke; bundle copies identical. | + +## 10. План реализации и тестов + +Предпочтительная граница — чистый helper в `src/plan-optimizer.ts` либо узком +optimizer-модуле. Он получает текущие rooms, rekeyed walls, resolved cuts и +scale constants, строит effective intervals, вычисляет все безопасные кандидаты +на исходном snapshot и возвращает новые exact entries. Общая wall geometry не +получает optimizer-specific ветку. + +Unit matrix добавляется в `test/plan-optimizer.test.mjs`; исходный fixture из +#197 должен сохранить точные 25 wall records на входе и доказать изменение +только целевого островка. Targeted smoke расширяет существующий Optimize smoke +либо получает отдельное имя, если так яснее проверяются Preview/Cancel/Apply/ +Undo. Golden не требуется: визуальный итог однозначно доказывается persisted +profile и существующим renderer; новый baseline принимается только перед бетой, +если реализация всё же добавит отдельную сцену. + +Mutation entry отключает вызов cleanup или его strict-threshold predicate: +чистая ветка зелёная, мутант возвращает прежние три entries и обязан падать на +focused unit. Полный smoke/golden/performance выполняется только перед бетой. + +## 11. Риски и производительность + +| Риск | Мера | +|---|---| +| Потеря намеренной точной границы | Все шесть условий §6 и negative matrix. | +| Каскад съест длинную зону | Одновременный non-cascading snapshot и запрет цепочек. | +| Vertex/cut tolerance зависит от scale | Матрица `coordScale = 1/NORM_W`, общий geometry tolerance. | +| Порядок записей меняет результат | Stable sorting и permutation test. | +| Cleanup протечёт в обычный Save/render | Helper вызывается только из `optimizePlans()`, AC8. | + +Проход выполняется только по явному admin-действию. Допустима полиномиальная +проверка соседства на текущих малых wall lists, но реализация не должна добавлять +работу в render/state tick. Security/privacy boundary, сеть и HA permissions не +меняются. + +## 12. Release-артефакты и rollback + +Изменение пользовательское. Implementation-коммит имеет `User-Visible: yes` и +включает: + +- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #198; +- `docs/USER-GUIDE.ru.md` — точное правило очистки в разделе Optimize; +- `docs/WALL-THICKNESS.md` — lossless runtime против явной optimizer cleanup; +- `docs/TESTING.md` — unit/smoke/mutation coverage; +- `docs/STATUS.md` — фактическую release-линию; +- unit, targeted smoke, mutation entry и три синхронные bundle copies. + +Отдельные i18n, backend, schema/migration, screenshot, security и performance +артефакты не нужны. Rollback — revert implementation-коммита; уже +оптимизированный единый 22-см entry остаётся валидным и может быть возвращён к +исходному виду только через сохранённую серверную Undo-копию либо backup. + +## 13. Принятые предположения + +1. «Геометрический узел» в первом этапе — room polygon vertex и endpoint + resolved open cut/open span; внутренний breakpoint, созданный только exact + wall entry, сам по себе не блокирует cleanup, иначе задача была бы пустой. +2. Соседство и длина проверяются по effective intervals после rekey, чтобы + preview описывал уже выровненную текущую геометрию. +3. Точная структура helper и имя targeted smoke являются техническим решением, + если AC и optimizer-only boundary сохраняются. +4. Нового report field нет: действующие `canonicalized` и `wallsMerged` + достаточно объясняют изменение без i18n/API расширения. +5. Продуктовых вопросов больше нет: Q1–Q3 приняты владельцем по defaults. diff --git a/docs/specs/README.md b/docs/specs/README.md index 02d6d219..1848ca3b 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -110,6 +110,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | Issue | ТЗ | |---|---| +| [#198](https://github.com/Matysh/houseplan-card/issues/198) Optimize очищает изолированный микро-интервал толщины | [198-optimize-micro-interval.md](198-optimize-micro-interval.md) | | [#103](https://github.com/Matysh/houseplan-card/issues/103) Состояния в Toggle confirmation | [103-toggle-confirmation-state.md](103-toggle-confirmation-state.md) | | [#200](https://github.com/Matysh/houseplan-card/issues/200) Одинаковая геометрия подписи комнаты в View и Plan editor | [200-room-label-parity.md](200-room-label-parity.md) |