Волна 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
16 KiB
Issue #198 — очистка изолированного микро-интервала толщины через Optimize
- Дата: 2026-08-19
- Тип: bug / maintenance canonicalisation · приоритет P3
- Оценка: пользовательская ценность 3/10 · ценность для разработки 5/10 · сложность 5/10 · риск 7/10
- Issue: #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 может получить толщину соседей, только если одновременно выполнены условия:
- его геометрическая длина строго меньше
0.5 × GRID_PITCHв render scale (эквивалентно0.5 × GRID_STEP_Nв persisted scale); - непосредственно слева и справа существуют положительные solid intervals;
- оба соседа коллинеарны центру, соприкасаются с его endpoints в действующем scale-relative tolerance и имеют одну одинаковую толщину;
- центральная толщина отличается от толщины соседей;
- ни один endpoint центра не является вершиной room polygon, концом
open_span/resolved open cut или иным топологическим разрывом текущего effective wall profile; - все три 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. Принятые предположения
- «Геометрический узел» в первом этапе — room polygon vertex и endpoint resolved open cut/open span; внутренний breakpoint, созданный только exact wall entry, сам по себе не блокирует cleanup, иначе задача была бы пустой.
- Соседство и длина проверяются по effective intervals после rekey, чтобы preview описывал уже выровненную текущую геометрию.
- Точная структура helper и имя targeted smoke являются техническим решением, если AC и optimizer-only boundary сохраняются.
- Нового report field нет: действующие
canonicalizedиwallsMergedдостаточно объясняют изменение без i18n/API расширения. - Продуктовых вопросов больше нет: Q1–Q3 приняты владельцем по defaults.