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

17 KiB
Raw Blame History

ТЗ #431 — канонизация координат kind: image

Issue: #431

Статус документа: реализовано.

Источники контракта: ТЗ #51, ТЗ #224 и ТЗ #291.

Сценарий

Автор плана добавляет собственное изображение в редакторе Подложки, двигает, масштабирует или поворачивает его и сохраняет конфигурацию. Позже он повторно сохраняет план без геометрических изменений либо запускает «Оптимизировать планы».

Сейчас 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-видов:

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-изменения.