Files
houseplan-card/docs/specs/431-image-coordinate-canonicalization.md
T
Codex d4dd027b0a build: prepare v1.71.0-beta.2 candidate
Issue: #426
Issue: #427
Issue: #428
Issue: #431
Issue: #432
Issue: #434
User-Visible: no
2026-09-03 15:23:40 +03:00

238 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ #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-изменения.