mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
230 lines
21 KiB
Markdown
230 lines
21 KiB
Markdown
# Issue #223 — Optimize канонизирует координаты без floating-point шума
|
||
|
||
- Дата: 2026-08-20
|
||
- Тип: bug / maintenance canonicalisation · приоритет P1
|
||
- Оценка: пользовательская ценность 8/10 · ценность для разработки 7/10 · сложность 4/10 · риск 4/10
|
||
- Issue: [#223](https://github.com/Matysh/houseplan-card/issues/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()` одновременно выполнено:
|
||
|
||
1. конечный результат `snapN(v)` численно отличается от входа;
|
||
2. абсолютная разница не превышает `EPS`;
|
||
3. результат действительно записан в 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. Принятые предположения
|
||
|
||
1. `coordsCanonicalized` считает координатные компоненты/узлы, а не объекты:
|
||
именно это даёт детерминированный отчёт для polygon и box call sites.
|
||
2. Near-node означает `abs(s - v) <= EPS`; граница включительна и не меняет
|
||
действующую константу tolerance.
|
||
3. Существующий `canonicalized` переименовывается только в пользовательском
|
||
тексте: структура и семантика report field не меняются.
|
||
4. Новый счётчик включается в существующее число «обслужено записей» итогового
|
||
toast, хотя единица там исторически агрегирует разные виды обслуживания.
|
||
5. Точное имя targeted smoke и размещение tracked-механизма являются техническим
|
||
решением, если AC и explicit-Optimize boundary сохраняются.
|
||
6. Продуктовых вопросов нет; эти предположения технические и могут быть свободно
|
||
скорректированы на ревью без дополнительного решения владельца.
|