mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
docs(spec): define image coordinate canonicalization
Issue: #431 User-Visible: no
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
# ТЗ #431 — канонизация координат `kind: image`
|
||||
|
||||
Issue: [#431](https://github.com/Matysh/houseplan-card/issues/431)
|
||||
|
||||
Статус документа: ТЗ на ревью.
|
||||
|
||||
Источники контракта: [ТЗ #51](051-custom-decor-images.md),
|
||||
[ТЗ #224](224-config-coordinate-canonicalization.md) и
|
||||
[ТЗ #291](291-lattice-coordinate-write-barrier.md).
|
||||
|
||||
## Сценарий
|
||||
|
||||
Автор плана добавляет собственное изображение в редакторе Подложки, двигает,
|
||||
масштабирует или поворачивает его и сохраняет конфигурацию. Позже он повторно
|
||||
сохраняет план без геометрических изменений либо запускает «Оптимизировать
|
||||
планы».
|
||||
|
||||
Сейчас `kind: image` проходит frontend- и backend-барьеры записи вне списка
|
||||
box-декора. Незаметные хвосты чисел с плавающей точкой сохраняются, поэтому
|
||||
повторный no-op жест или оптимизация способен снова дать технический diff и
|
||||
лишнюю ревизию.
|
||||
|
||||
После исправления изображение следует тому же контракту `x/y/w/h/angle`, что
|
||||
`rect`, `ellipse` и `furniture`: следующая штатная запись или Optimize приводит
|
||||
его координаты к канонической форме, а повтор операции является no-op.
|
||||
|
||||
## Что человек увидит до и после
|
||||
|
||||
| Ситуация | Сейчас | После исправления |
|
||||
|---|---|---|
|
||||
| Сохранение изображения после drag/resize/rotate | План выглядит правильно, но в конфиг могут попасть шумовые float-координаты | Геометрия сохраняется канонически без визуального сдвига |
|
||||
| Повторный no-op жест или Optimize | Может появиться ещё одно изменение/ревизия того же плана | Повторная операция не создаёт нового геометрического diff |
|
||||
| Старый план с шумовыми координатами изображения | Шум остаётся после обычной записи | Нормализуется при следующей записи или явном Optimize |
|
||||
|
||||
Новых кнопок, сообщений, настроек и визуальных состояний нет.
|
||||
|
||||
## Подтверждённая причина
|
||||
|
||||
- `DecorKind` уже содержит `image`, а `DecorImage` использует box-поля
|
||||
`x/y/w/h/angle`.
|
||||
- `src/coordinate-canonicalization.ts` не включает `image` ни в сбор значений
|
||||
для `latticeCanonicalizationReport()`, ни в реальную канонизацию config.
|
||||
- `custom_components/houseplan/coordinate_canonicalization.py` содержит то же
|
||||
неполное зеркало.
|
||||
- Общая fixture перечисляет остальные четыре вида декора, но не `image`, поэтому
|
||||
frontend и backend согласованно подтверждают один и тот же дефект.
|
||||
|
||||
## Скоуп
|
||||
|
||||
- включить `image` в box-контракт frontend-сбора статистики и канонизации;
|
||||
- включить `image` в Python-зеркало канонизации;
|
||||
- сделать полный набор box-видов явным и проверяемым, чтобы обходы не содержали
|
||||
независимые цепочки сравнений;
|
||||
- расширить shared fixture и оба runtime-набора тестов;
|
||||
- добавить отрицательные доказательства, что выпадение вида из frontend либо
|
||||
backend краснит соответствующий тест;
|
||||
- уточнить compatibility-документацию и добавить парную changelog-запись.
|
||||
|
||||
## Не-скоуп
|
||||
|
||||
- изменение формата `DecorImage`, asset API, загрузки, рендера или редактора;
|
||||
- новая миграция, повышение model/config/export version либо запись при чтении;
|
||||
- изменение точности, порога lattice snap или формулы канонизации;
|
||||
- рекурсивное округление неизвестных числовых полей;
|
||||
- канонизация `opacity`, `width_cm`, `flip_h`, `flip_v`, `asset_id` или других
|
||||
presentation/content-полей;
|
||||
- изменение поведения неизвестных и будущих `decor.kind` без отдельной
|
||||
классификации их геометрии.
|
||||
|
||||
## Контракт поведения
|
||||
|
||||
### 1. Каталог геометрических классов декора
|
||||
|
||||
Frontend имеет один runtime-каталог box-видов:
|
||||
|
||||
```text
|
||||
rect · ellipse · furniture · image
|
||||
```
|
||||
|
||||
Тип `DecorKind` обязан получать эти варианты из того же каталога, а не повторять
|
||||
отдельный независимый список. Сбор статистики и фактическая канонизация используют
|
||||
один predicate/каталог. Python объявляет точное зеркало box-набора.
|
||||
|
||||
Shared contract перечисляет ожидаемый box-набор. Frontend unit и backend test
|
||||
сверяют с ним свои runtime-каталоги exact-set сравнением и прогоняют одинаковую
|
||||
геометрию для каждого вида. Удаление одного вида из любого runtime-каталога либо
|
||||
рассинхронизация shared contract обязаны дать красный тест.
|
||||
|
||||
### 2. Поля и числовой контракт
|
||||
|
||||
Для каждого box-вида, включая `image`:
|
||||
|
||||
- `x`, `y`, `w`, `h` проходят существующую lattice-канонизацию относительно
|
||||
`1/240` с действующим порогом;
|
||||
- `angle` проходит существующую scalar-канонизацию до девяти десятичных знаков;
|
||||
- near-node значения учитываются в `latticeCanonicalizationReport()` как
|
||||
`canonicalized`, а намеренно off-grid значения — как `far` без snap;
|
||||
- повторная канонизация результата byte-equivalent и идемпотентна.
|
||||
|
||||
Все остальные поля image record сохраняются без изменений. Невалидные,
|
||||
нечисловые и non-finite значения продолжают обрабатываться действующей схемой;
|
||||
эта задача не меняет её политику валидации.
|
||||
|
||||
### 3. Пути записи и Optimize
|
||||
|
||||
Новые специальные writer-ветки не добавляются. Исправление действует через
|
||||
существующие общие барьеры:
|
||||
|
||||
- frontend config candidate до `houseplan/config/set`;
|
||||
- backend config schema и storage helper;
|
||||
- предварительную и финальную канонизацию Optimize;
|
||||
- сбор отчёта Optimize о lattice-изменениях.
|
||||
|
||||
Существующие route guards #291 остаются без изменений: задача исправляет полноту
|
||||
данных внутри барьера, а не инвентарь writer-ов.
|
||||
|
||||
## Совместимость и миграция
|
||||
|
||||
- Новых полей и миграции нет; model/config/export versions не меняются.
|
||||
- Старые конфиги читаются byte-for-byte как раньше. Image geometry становится
|
||||
канонической только при следующей штатной записи или явном Optimize.
|
||||
- Уже канонические изображения не меняются.
|
||||
- Старые версии House Plan продолжают читать результат как обычный
|
||||
`kind: image`; downgrade не требует обратной миграции.
|
||||
|
||||
## Touch, accessibility, i18n и производительность
|
||||
|
||||
- Touch/View/kiosk и доступность не меняются: жесты и рендер остаются прежними.
|
||||
- Новых строк и ключей i18n нет.
|
||||
- Новых обходов config нет. Четырёхэлементный membership-check заменяет текущую
|
||||
цепочку сравнений внутри уже существующих обходов; бюджеты производительности
|
||||
не меняются.
|
||||
- Security/privacy и сетевые поверхности не затрагиваются.
|
||||
|
||||
## Затронутые файлы и модули
|
||||
|
||||
- `src/editors/decor/types.ts` — единый runtime box-каталог и производные типы;
|
||||
- `src/coordinate-canonicalization.ts` — использование каталога при сборе и
|
||||
канонизации;
|
||||
- `custom_components/houseplan/coordinate_canonicalization.py` — Python-зеркало;
|
||||
- `test/fixtures/coordinate-canonicalization.json` — shared набор и image row;
|
||||
- `test/coordinate-canonicalization.test.mjs` — frontend completeness,
|
||||
idempotency и preservation;
|
||||
- `tests_backend/test_coordinate_canonicalization.py` — backend mirror и schema;
|
||||
- `scripts/mutation-gate.mjs` — постоянный свидетель backend-защиты;
|
||||
- `docs/CONFIG-COMPATIBILITY.md` — явный image box-контракт;
|
||||
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` — release note;
|
||||
- этот файл и `docs/specs/README.md` — трассируемость.
|
||||
|
||||
User Guide, i18n, screenshots/golden, smoke и performance fixtures не меняются.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- **AC1 — frontend image canonicalization (unit).** `image.x/y/w/h` получают
|
||||
тот же lattice-результат, а `image.angle` тот же scalar-результат, что
|
||||
эквивалентный `furniture`; immutable и in-place API дают одинаковый результат.
|
||||
- **AC2 — отчёт и идемпотентность (unit).** Image near-node/off-grid значения
|
||||
правильно входят в `latticeCanonicalizationReport()`; повторная
|
||||
канонизация/Optimize не создаёт изменений.
|
||||
- **AC3 — backend mirror (backend).** Python helper и `CONFIG_SCHEMA` дают для
|
||||
image record точный shared expected result и сохраняют его идемпотентно.
|
||||
- **AC4 — полнота box-набора (unit + backend + mutation).** Runtime-каталоги TS
|
||||
и Python exact-set равны shared contract `rect/ellipse/furniture/image`, а
|
||||
каждый вид реально прогоняется через `x/y/w/h/angle`. Удаление `image` из TS
|
||||
краснит targeted unit; удаление из Python краснит targeted backend test через
|
||||
зарегистрированный mutation-gate witness.
|
||||
- **AC5 — поля вне геометрии (unit + backend).** `asset_id`, `opacity`,
|
||||
`flip_h`, `flip_v` и неизвестное extension-поле переживают обе канонизации без
|
||||
изменений; неизвестный `decor.kind` не начинает округляться рекурсивно.
|
||||
- **AC6 — совместимость (ревью кода).** Формула, пороги, версии, схемы данных,
|
||||
writer inventory, UI и i18n не меняются; действующие тесты #224/#248/#291
|
||||
остаются зелёными.
|
||||
- **AC7 — документация и release (docs gate + ревью кода).** Compatibility doc
|
||||
называет `image` среди box-видов; оба changelog обновлены в том же
|
||||
`User-Visible: yes` implementation commit.
|
||||
- **AC8 — гейты (commands + Linux CI).** Зелёные `npm run typecheck`,
|
||||
`npm test`, `npm run build`, targeted backend test и оба отрицательных
|
||||
свидетеля. Полный HA harness каноничен в Linux CI.
|
||||
|
||||
## План автотестов и таблица защитных свидетелей
|
||||
|
||||
1. Добавить в shared fixture `boxKinds` и representative `image` с шумом во
|
||||
всех пяти геометрических полях и отдельными полями, которые менять нельзя.
|
||||
2. В frontend unit сравнить runtime-каталог с `boxKinds`, затем
|
||||
параметризованно проверить каждый вид и image preservation.
|
||||
3. В backend test сравнить Python-зеркало с тем же `boxKinds`, проверить helper
|
||||
и `CONFIG_SCHEMA` на том же expected output.
|
||||
4. Добавить Optimize/no-op проверку для image: первый прогон очищает измеримый
|
||||
шум, второй возвращает отсутствие persisted changes.
|
||||
5. Выполнить отрицательные прогоны до передачи на код-ревью:
|
||||
|
||||
| Защитный AC | Чем доказан | Чем обязан краснеть |
|
||||
|---|---|---|
|
||||
| AC4 frontend completeness | targeted `coordinate-canonicalization` unit | удалить `image` из TS box-каталога → unit fail |
|
||||
| AC4 backend completeness | targeted backend test | mutation-gate: удалить `image` из Python box-каталога → backend fail |
|
||||
| AC5 allowlist boundary | unit + backend preservation cases | заменить box-ветку рекурсивным округлением/задеть extension field → preservation fail |
|
||||
|
||||
## Release-артефакты
|
||||
|
||||
- В `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` — парная запись: координаты
|
||||
пользовательских изображений теперь проходят общий стабильный барьер записи и
|
||||
не создают повторный технический diff.
|
||||
- В `docs/CONFIG-COMPATIBILITY.md` — `image` явно включён в действующий
|
||||
`x/y/w/h/angle` box-контракт.
|
||||
- Screenshots/golden и User Guide не обновляются: визуал и пользовательский поток
|
||||
не меняются.
|
||||
- Release/version/tag не входят в задачу; issue остаётся открытой в S8 до беты.
|
||||
|
||||
## Риски
|
||||
|
||||
1. **Исправлен writer, но не отчёт.** Один predicate обязан использоваться обоими
|
||||
TS-обходами; AC2 проверяет счётчик Optimize.
|
||||
2. **Frontend и backend снова расходятся одинаково незаметно.** Exact-set
|
||||
сравнение обоих runtime-каталогов с одной fixture и backend-мутант делают
|
||||
удаление наблюдаемым.
|
||||
3. **Случайно канонизированы presentation/content-поля.** AC5 фиксирует
|
||||
allowlist и неизвестное extension-поле.
|
||||
4. **Тест проверяет список, но не поведение.** AC4 требует прогнать каждый вид,
|
||||
а не ограничиваться сравнением строк каталога.
|
||||
|
||||
## Откат
|
||||
|
||||
Откат — revert implementation commit. Новых полей и миграций нет; уже
|
||||
канонизированные image-координаты остаются валидными. Цена отката — возврат
|
||||
floating-point шума для следующих записей изображений.
|
||||
|
||||
## Принятые предположения
|
||||
|
||||
- Box-геометрия определяется структурой `x/y/w/h/angle`; текущий полный набор —
|
||||
`rect`, `ellipse`, `furniture`, `image`.
|
||||
- `angle` остаётся scalar, а не lattice-полем; `flip_h/flip_v` не кодируются
|
||||
отрицательными размерами и не канонизируются.
|
||||
- Shared fixture является языконезависимым тестовым контрактом; продуктовый
|
||||
runtime не читает fixture с диска.
|
||||
- Исправление считается пользовательским bugfix (`User-Visible: yes`), хотя
|
||||
визуальный кадр не меняется: оно устраняет наблюдаемые лишние сохранения и
|
||||
повторные Optimize-изменения.
|
||||
@@ -166,6 +166,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#419](https://github.com/Matysh/houseplan-card/issues/419) Безопасная уборка Area-снапшота при пустом или усечённом HA-реестре | [419-area-snapshot-roster-guard.md](419-area-snapshot-roster-guard.md) |
|
||||
| [#421](https://github.com/Matysh/houseplan-card/issues/421) Отрицательные доказательства для трёх защитных проверок | [421-negative-test-proofs.md](421-negative-test-proofs.md) |
|
||||
| [#426](https://github.com/Matysh/houseplan-card/issues/426) Отключение информационного окна комнаты при наведении | [426-room-hover-tooltip-toggle.md](426-room-hover-tooltip-toggle.md) |
|
||||
| [#431](https://github.com/Matysh/houseplan-card/issues/431) Канонизация координат пользовательских изображений | [431-image-coordinate-canonicalization.md](431-image-coordinate-canonicalization.md) |
|
||||
|
||||
## P3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user