mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
Issue: #426 Issue: #427 Issue: #428 Issue: #431 Issue: #432 Issue: #434 User-Visible: no
238 lines
17 KiB
Markdown
238 lines
17 KiB
Markdown
# ТЗ #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-изменения.
|