21 KiB
Issue #223 — Optimize канонизирует координаты без floating-point шума
- Дата: 2026-08-20
- Тип: bug / maintenance canonicalisation · приоритет P1
- Оценка: пользовательская ценность 8/10 · ценность для разработки 7/10 · сложность 4/10 · риск 4/10
- Issue: #223
- Ветка:
issue/223-optimize-coordinate-canonicalization
Канонические документы: docs/SCOPE.md, docs/CANVAS.md,
docs/USER-GUIDE.ru.md, docs/CONFIG-COMPATIBILITY.md, docs/TESTING.md.
1. Сценарий, персона и момент
Администратор обслуживает старый либо импортированный план через явное действие «Общие настройки → Оптимизировать планы». Координаты комнат визуально уже лежат на сетке, но содержат накопленный IEEE-754 шум величиной в один или несколько ULP. Пользователь ожидает, что Optimize сохранит геометрию без видимого сдвига, запишет канонические узлы сетки и тем самым восстановит надёжную работу объединения комнат, Glow и другой downstream boolean-геометрии.
2. Что человек увидит до и после
До: Optimize считает значения в пределах EPS уже выровненными и возвращает
исходные числа бит-в-бит. В воспроизводимом плане из шести комнат preview
сообщает, что изменений нет, хотя шум около 5.5e-17 сохраняется и объединение
комнат может не распознать общую границу.
После: тот же явный Optimize предлагает изменение без видимого перемещения:
сдвинуто элементов — 0, но отдельно сообщает количество канонизированных
координат. После подтверждения общие вершины имеют ровно одинаковые числовые
значения, объединение комнат и Glow не зависят от прежнего ULP-шума. Повторный
Optimize сообщает, что изменений нет.
3. Подтверждённая причина
snapN() в src/align-grid.ts вычисляет ближайший узел s, но при
abs(s - v) <= EPS возвращает v. Защитное правило было введено ради
идемпотентности, однако фактически консервирует почти канонический ввод. Это
противоречит обещанию явного Optimize из docs/CANVAS.md: maintenance-действие
должно переписывать persisted-геометрию в каноническую форму.
Потребительская устойчивость из #218 остаётся необходимой страховкой на чтении, но не очищает источник данных. #199 — будущая общая проверка кандидата перед записью, а не исправление данной канонизации. Задачи не являются дубликатами.
4. Scope
snapN()всегда возвращает вычисленный ближайший узел сетки для конечного числа, в том числе когда отличие не превышаетEPS;alignAllToGrid()отдельно считает канонические замены без заметного перемещения и учитывает их вchanged;AlignReportиOptimizeReportполучают полеcoordsCanonicalized;- preview и итоговый toast Optimize на русском и английском объясняют случай
moved = 0,changed = true; - сохраняются preview/Cancel/Apply/server Undo, идемпотентность, неизвестные поля конфигурации и действующая model/schema compatibility;
- добавляются unit, targeted production-bundle smoke и mutation guard;
- актуализируются документы о канонизации координат.
5. Non-scope
- автоматическая канонизация при каждом Save, импорте, чтении или рендере;
- изменение шага сетки,
EPS, координатной системы или допустимого пользовательского размещения между узлами; - замена устойчивых geometry-сравнений из #218 строгим равенством;
- общая pre-apply валидация/барьер из #199;
- исправление иных optimizer-pass, стен, проёмов либо миграций модели;
- новая persisted-схема, model-version, backend API или HA permission;
- новый control, жест, route либо предупреждение помимо существующего preview.
6. Контракт канонизации и отчёта
Для каждого конечного нормализованного числа v, переданного в snapN(),
канонический результат равен:
Math.round(v / GRID_STEP_N) * GRID_STEP_N.
Результат должен быть точно равен этому вычисленному узлу (===), а повторное
применение не должно менять значение. NaN и бесконечности сохраняют прежнее
поведение и возвращаются без преобразования.
coordsCanonicalized — число отдельных координатных компонент/узлов, для
которых в ходе alignAllToGrid() одновременно выполнено:
- конечный результат
snapN(v)численно отличается от входа; - абсолютная разница не превышает
EPS; - результат действительно записан в candidate.
Считается каждое место использования координаты, а не уникальное числовое
значение и не объект целиком. Для прямоугольника его ближняя и дальняя стороны
считаются как координатные компоненты, даже когда дальняя сторона вычислена как
x + w. Заметное выравнивание с разницей > EPS остаётся только в moved и не
дублируется в coordsCanonicalized. Изменение угла проёма, удаление draft и
канонизация стен/спанов не входят в новый счётчик.
AlignResult.changed истинно при moved > 0 или
coordsCanonicalized > 0; остальные существующие причины изменения сохраняют
свои текущие контракты. В optimizePlans() новый счётчик переносится без
потери в OptimizeReport. maxShift, maxShiftCm и maxSpace не растут от
замен в пределах EPS: пользовательское обещание о физическом сдвиге остаётся
честным.
7. UX, запись и Undo
Preview остаётся существующим диалогом. В строку обслуживания добавляется
отдельный показатель, а существующий счётчик canonicalized получает
однозначное название по своей единице измерения:
- RU: «обновлено пространств: {c}; устранён шум координат: {p}»;
- EN: «spaces updated: {c}; noisy coordinate values removed: {p}».
c по-прежнему означает число пространств, где переписано представление
open_spans/open_to/walls; p означает число отдельных coordinate values
по §6. Два разных показателя не используют один термин «канонизировано» в одном
предложении. Оба выводятся в общей строке всегда, включая ноль, так же как
действующие счётчики миграций/стен/виртуальных фрагментов. При единственном
изменении из-за ULP-шума диалог не показывает «изменений нет»: он показывает
нулевой видимый сдвиг и положительный coordsCanonicalized.
Итоговый toast использует новый показатель в сумме обслуженных записей, поэтому
после Apply не сообщает 0 обслуженных записей. Cancel не пишет candidate.
Apply отправляет тот же exact candidate через существующую backend-транзакцию;
Undo возвращает исходные noisy-значения в рамках действующего срока жизни
резервной копии.
Новых focus, keyboard, touch и screen-reader взаимодействий нет. Действующий admin-only safety floor и подтверждение сохраняются.
8. Данные, compatibility и миграция
Persisted schema и PLAN_MODEL_VERSION не меняются. Числа остаются обычными
JSON number; меняется только их exact representation после добровольного
Optimize. Неизвестные поля, backdrop calibration, view boxes, unattached layout
entries и файлы сохраняются. Входные config и layout не мутируются.
Старые версии карточки читают получившиеся координаты как валидные значения на
той же сетке. Фоновая миграция не запускается. Повторный optimizePlans() над
собственным результатом возвращает глубокий эквивалент, changed: false и
coordsCanonicalized: 0.
9. Acceptance criteria
| AC | Критерий | Доказательство |
|---|---|---|
| AC1 | Для канонического узла с добавленным/вычтенным ULP snapN() возвращает ближайший вычисленный узел точно, а не исходный шум. |
Focused align-grid unit; mutant snapn-returns-input-near-node. |
| AC2 | Значение дальше EPS продолжает выравниваться как раньше; не конечные числа сохраняют прежний результат. |
Boundary unit matrix. |
| AC3 | Воспроизводимый fixture из шести комнат после Optimize имеет точно совпадающие общие вершины, changed: true, moved: 0, coordsCanonicalized > 0; union/downstream geometry успешно строится. |
plan-optimizer regression unit на fixture из #223/#218. |
| AC4 | Счётчик считает отдельные компоненты только при разнице <= EPS, не дублирует заметно перемещённые элементы и не увеличивает maxShift*. Принятая партиция и wall_column учитываются; у партиции с hostedFit = false вычисленный, но не записанный snap не учитывается и noisy endpoints сохраняются. |
Units для room poly, rect/decor/layout, применённой и отклонённой partition, wall column и off-grid negative case. |
| AC5 | Второй запуск над candidate возвращает changed: false, coordsCanonicalized: 0 и побитово/глубоко тот же JSON. |
Idempotence unit. |
| AC6 | RU/EN preview разными терминами показывает «обновлённые пространства» (canonicalized) и «устранённый шум координат» (coordsCanonicalized); случай moved=0 не превращается в «нет изменений»; итоговый toast учитывает очищенные координаты. |
i18n/UI unit + targeted production-bundle browser smoke. |
| AC7 | Preview не мутирует входы; Cancel ничего не пишет; Apply сохраняет exact candidate; Undo восстанавливает исходные noisy-координаты и неизвестные поля. | Optimizer immutability unit + targeted browser/backend smoke. |
| AC8 | Обычные read/render/Save без явного Optimize не переписывают persisted config; устойчивость #218 остаётся зелёной. | Existing regression suite + focused negative unit. |
| AC9 | Документация описывает явную exact-канонизацию и значение нового счётчика; три bundle-копии синхронны. | Docs check, bundle parity check. |
| AC10 | Рабочие gates зелёные. | typecheck, unit, build, targeted smoke, mutation gate. |
10. План реализации и тестов
В alignAllToGrid() вводится локальный tracked-механизм над чистым snapN().
Он возвращает snapped value и признак near-node replacement отдельно, а вклад в
coordsCanonicalized подтверждается только в месте фактической записи значения
в candidate. В частности, partitions добавляет вклад четырёх endpoints лишь в
ветке snappedLength > EPS && hostedFit; отклонённая ветка оставляет и координаты,
и счётчик без изменения. wall_columns и остальные безусловно записываемые
grid-bound call sites подтверждают вклад сразу после присваивания. Счётчик
остаётся контекстом одного maintenance-pass и не добавляет глобального состояния
в базовый helper.
Unit matrix расширяется в test/align-grid.test.mjs и
test/plan-optimizer.test.mjs. Реальный noisy fixture должен сохранять точные
исходные числа, чтобы тест падал при возврате старой ветки return v.
Targeted smoke запускает Optimize через собранный production bundle и проверяет
Preview/Cancel/Apply/Undo вместе с RU/EN строкой. Golden baseline не требуется:
компоновка диалога не меняется, добавляется текстовый счётчик; перед бетой
выполняется общий golden verify по процессу.
Mutation entry возвращает старое поведение near-node snapN() либо отключает
tracked increment. Чистая ветка зелёная, мутант обязан падать на focused unit.
Полные golden/smoke/performance выполняются перед бетой, а в implementation-
цикле — typecheck, unit, build и targeted smoke из этого ТЗ.
11. Риски, производительность и security
| Риск | Мера |
|---|---|
| Счётчик вводит пользователя в заблуждение как число объектов | Точное название «координат» и семантика отдельных компонент в §6. |
| Два разных счётчика выглядят одной категорией канонизации | Нейтральное «обновлено пространств» и точное «устранён шум координат» явно называют разные единицы и действия; занятый термин нормализованных координат не используется для c. |
| Отклонённый snap партиции попадает в отчёт | Вклад подтверждается только после фактической записи; отдельный hostedFit=false unit. |
| Near-node rewrite ошибочно считается физическим сдвигом | Раздельные counters; moved и maxShift* не растут при <= EPS. |
| Идемпотентность нарушается из-за недвоичного шага сетки | Результат определяется тем же выражением узла; exact second-run unit для всех call sites. |
| Часть координат остаётся noisy | Все grid-bound вызовы snapN() в batch переводятся на tracked wrapper; реальный fixture. |
| Изменится обычный Save/render | Helper вызывается только в существующем explicit Optimize pipeline; AC8. |
Проход остаётся линейным по уже обходившимся координатам и добавляет одну exact- проверку и сравнение на компоненту. Работы в render/state tick, новых сетевых запросов, данных третьим сторонам, HTML-инъекций или изменений HA permissions нет. Отдельный performance/security gate не требуется.
12. Release-артефакты и rollback
Изменение пользовательское. Implementation-коммит имеет User-Visible: yes и
включает:
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdсо ссылкой на #223;docs/USER-GUIDE.ru.md— exact-канонизацию и новый показатель Optimize;docs/CANVAS.md— разграничение live snapping и явного maintenance-pass;docs/TESTING.md— unit/smoke/mutation coverage;docs/STATUS.md— фактическую release-линию;- RU/EN i18n, unit, targeted smoke, mutation entry и три синхронные bundle- копии.
Новая schema/migration, backend, screenshot baseline, performance baseline и security artifact не нужны. Rollback — revert implementation-коммита. Уже канонизированные координаты остаются валидными и визуально эквивалентными; вернуть прежние noisy-биты можно только существующим Undo Optimize либо backup.
13. Принятые предположения
coordsCanonicalizedсчитает координатные компоненты/узлы, а не объекты: именно это даёт детерминированный отчёт для polygon и box call sites.- Near-node означает
abs(s - v) <= EPS; граница включительна и не меняет действующую константу tolerance. - Существующий
canonicalizedпереименовывается только в пользовательском тексте: структура и семантика report field не меняются. - Новый счётчик включается в существующее число «обслужено записей» итогового toast, хотя единица там исторически агрегирует разные виды обслуживания.
- Точное имя targeted smoke и размещение tracked-механизма являются техническим решением, если AC и explicit-Optimize boundary сохраняются.
- Продуктовых вопросов нет; эти предположения технические и могут быть свободно скорректированы на ревью без дополнительного решения владельца.