Files
houseplan-card/docs/specs/428-missing-decor-asset-roundtrip.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

267 lines
19 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.
# ТЗ #428 — round-trip экспорта с отсутствующей картинкой декора
Issue: [#428](https://github.com/Matysh/houseplan-card/issues/428)
Статус документа: ТЗ на ревью.
Источник контракта: [ТЗ #51](051-custom-decor-images.md), раздел
«Import/export и совместимость», AC10 и AC11.
## Сценарий
1. В конфигурации пространства сохранён `decor`-объект `kind: image` с
корректным 64-символьным SHA-256 `asset_id`.
2. Соответствующего blob и metadata sidecar уже нет в
`<config>/houseplan/assets/`.
3. Пользователь экспортирует полный дом, одно пространство либо только
планировку, а затем пытается импортировать полученный JSON.
Сейчас exporter честно записывает для объекта `exists_at_export: false` и
`mime: null`, но importer требует MIME из белого списка для любой строки
`decor_asset`. Поэтому House Plan отклоняет весь собственный экспорт как
`invalid_content`, хотя #51 определяет missing asset как легальное,
восстанавливаемое состояние.
После исправления документ проходит preview, сообщает об отсутствующем файле,
требует действующее явное подтверждение и сохраняет image-объект вместе с его
геометрией как repair-placeholder. Остальной импорт не меняется.
## Что человек увидит до и после
| Состояние | Сейчас | После исправления |
|---|---|---|
| Файл картинки отсутствовал уже при экспорте | Импорт всего JSON завершается ошибкой | Preview открывается, показывает missing content и требует подтверждение |
| Пользователь подтверждает импорт без файла | До подтверждения невозможно дойти | Объект и его геометрия сохраняются; во View не рисуется, в Background editor доступен для замены |
| Файл с тем же exact hash уже есть на целевой системе | Документ всё равно отклоняется из-за `mime: null` | Локальный blob проверяется по SHA-256 и переиспользуется как `available` |
Нового диалога, текста ошибки или элемента управления нет.
## Подтверждённая причина
- `custom_components/houseplan/import_export.py::content_manifest()` получает
MIME из metadata sidecar либо расширения найденного blob. Если оба файла
отсутствуют, результат — `None`; флаг `exists_at_export` при этом равен
`False`.
- `_content_state()` повторно строит ожидаемые ссылки из payload, но затем
безусловно требует у supplied `decor_asset` MIME из множества
`image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`.
- `test_issue_51_missing_decor_asset_stays_as_repairable_geometry` покрывает
только соседний случай: blob был у источника и потому MIME был известен, но
blob отсутствует на target.
## Скоуп
- ограниченно скорректировать валидацию `decor_asset` в `_content_state()`;
- сохранить строгую сверку manifest с image-ссылками, заново выведенными из
payload;
- покрыть настоящий round-trip «export при отсутствующем blob/sidecar → import
preview» и отрицательную матрицу manifest;
- уточнить контракт missing decor asset в `docs/CONFIG-COMPATIBILITY.md`,
`docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`;
- добавить пользовательскую запись в оба changelog.
## Не-скоуп
- встраивание blob/base64 в JSON;
- восстановление, загрузка, перенос или автоматическое удаление asset-файлов;
- угадывание MIME из `asset_id`: content-addressed id не содержит расширение;
- изменение формата export, `EXPORT_FORMAT_VERSION`, config/model schema либо
storage layout;
- изменение UI подтверждения, placeholder, счётчиков preview, delete/replace,
full/space/plan-only projection или политики внешних файлов;
- ослабление проверки любых manifest-строк, кроме строго описанного ниже
missing `decor_asset`.
## Контракт manifest и валидации
### 1. Канонический экспорт
Exporter продолжает выдавать одну extension-neutral строку `decor_asset` на
каждый image record. Поля `asset_id` и `hash` равны canonical lowercase SHA-256,
`exists_at_export` всегда имеет настоящий тип `bool`.
- Если verified source blob существует, `exists_at_export` равно `true`, а
`mime` обязательно входит в поддерживаемый белый список.
- Если verified source blob отсутствует либо не проходит exact hash,
`exists_at_export` равно `false`. `mime` может быть поддерживаемой строкой,
когда её сохранил валидный metadata sidecar, либо JSON `null`, когда MIME
достоверно неизвестен.
Exporter не восстанавливает MIME эвристикой и не добавляет bytes.
### 2. Допустимые строки при импорте
До определения локального состояния target importer проверяет supplied
`decor_asset` по следующей матрице:
| `exists_at_export` | `mime` | Результат |
|---|---|---|
| literal `true` | поддерживаемая строка | допустимо |
| literal `true` | отсутствует, `null` или неподдерживаемая строка | `ImportFailure("invalid_content")` |
| literal `false` | поддерживаемая строка | допустимо |
| literal `false` | отсутствует или `null` | допустимо: это исправляемый missing asset |
| literal `false` | любая неподдерживаемая строка, включая `""`, либо значение другого типа | `ImportFailure("invalid_content")` |
| поле отсутствует, `null`, `0`, `1`, строка, объект или массив | любое | `ImportFailure("invalid_content")` |
Во всех допустимых строках остаются обязательными:
- exact equality supplied `asset_id` и `hash` с SHA-256, выведенным из payload;
- exact identity строки (`kind`, `owner`, `owner_id`, `field`, `url`) и отсутствие
лишних/дублированных/пропущенных строк;
- повторная проверка target blob чтением bytes и сравнением SHA-256.
Поддерживаемый MIME — только `image/png`, `image/jpeg`, `image/webp` или
`image/svg+xml`. Неподдерживаемый указанный MIME нельзя маскировать
`exists_at_export: false`.
### 3. Локальное состояние target
Источник не определяет доступность на целевой системе:
- target blob с exact hash → `state: available`, `exists_on_target: true`,
отдельное подтверждение для этой строки не требуется;
- target blob отсутствует, нечитаем или hash не совпадает →
`state: missing_preserved`, `exists_on_target: false`, общий preview получает
`confirmation_required: true`;
- после подтверждения `_detach_missing()` не удаляет image record: `asset_id`,
geometry, opacity, flips и decor order сохраняются по контракту #51.
Значение `exists_at_export` в нормализованной preview-строке остаётся значением
из supplied manifest. `mime: null` не превращается в MIME найденного либо
предполагаемого файла и не становится authority.
## Совместимость и миграция
- Миграции config/model/storage нет.
- Номер export format не меняется: исправленный importer принимает ранее
сгенерированный самим House Plan v2 документ, который уже соответствовал
заявленному контракту `exists_at_export: false`.
- Все документы, которые принимались раньше, продолжают приниматься.
- Старый importer может по-прежнему отвергнуть такой JSON; исправление не может
сделать уже установленную старую версию совместимой вперёд.
- Отсутствующий `exists_at_export` не трактуется как legacy default: exporter
v2 всегда записывает поле, а fail-closed поведение защищает границу доверия.
## Безопасность и privacy
Manifest остаётся описательным, не авторитетным. Payload определяет полный набор
ссылок, а target bytes — фактическую доступность. Исключение для отсутствующего
MIME связано одновременно с exact image identity и literal false; оно не даёт
подсунуть внешний URL, пропустить ссылку, объявить другой hash/MIME или обойти
проверку файла. Новых данных в export и новых путей к файловой системе нет.
## Touch, accessibility, i18n и производительность
- Touch/View/kiosk не меняются: это backend round-trip до существующего preview.
- Новых текстов и ключей i18n нет; используются действующие missing-content
confirmation и repair-placeholder.
- На каждый asset остаются тот же один bounded поиск кандидатов и, при наличии
blob, один SHA-256 проход. Новых обходов, сетевых запросов и frontend bundle
кода нет; performance budgets не меняются.
## Затронутые файлы и модули
- `custom_components/houseplan/import_export.py` — bounded validation
`decor_asset` в `_content_state()`, полная image-проекция plan-only и её
ограниченный manifest allowlist;
- `tests_backend/test_ha_import_export.py` — положительный round-trip и
отрицательная матрица;
- `docs/CONFIG-COMPATIBILITY.md` — точное значение missing MIME;
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md` — пользовательское правило
повторного экспорта/import missing image;
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` — release note;
- этот файл и `docs/specs/README.md` — трассируемость ТЗ.
Frontend `src/**`, i18n JSON, version files, screenshots/golden и workflow не
затрагиваются.
## Критерии приёмки
- **AC1 — настоящий missing round-trip (backend).** Full export, созданный при
отсутствии blob и sidecar, содержит `exists_at_export: false, mime: null`;
его import preview не падает, выдаёт `missing_preserved` и требует
подтверждение.
- **AC2 — сохранение объекта (backend).** После подтверждённой подготовки
импорта image record сохраняет exact `asset_id`, geometry, opacity, flip flags
и decor order; `_detach_missing()` его не удаляет.
- **AC3 — три режима экспорта (backend).** Общая validation path доказана для
full, single-space и plan-only export: missing image row во всех режимах
импортируема по одному контракту, а projection/privacy каждого режима не
меняются.
- **AC4 — target reuse (backend).** Для строки
`exists_at_export: false, mime: null` existing target blob переиспользуется
только после exact SHA-256 проверки и получает `available` без ложного
missing confirmation.
- **AC5 — fail-closed MIME/flag (backend).** Параметризованный отрицательный
тест доказывает матрицу: null/omitted MIME допустим только при literal false;
unsupported non-null MIME отклоняется и при false; отсутствующий и любой
не-bool `exists_at_export` отклоняются.
- **AC6 — identity invariants (backend).** Mismatched `asset_id`/`hash`, лишняя,
пропущенная или дублированная manifest row по-прежнему дают
`invalid_content`; существующее покрытие остаётся зелёным.
- **AC7 — совместимость (backend + ревью кода).** Ранее допустимые строки
`exists_at_export: true` с поддерживаемым MIME и `false` с поддерживаемым MIME
не меняют результат; config/model/export version не повышается.
- **AC8 — документация и release (docs gate + ревью кода).** Оба User Guide и
compatibility doc описывают импортируемый missing round-trip; оба changelog
обновлены в том же `User-Visible: yes` implementation commit.
- **AC9 — гейты (commands + Linux CI).** В цикле реализации зелёные
`npm run typecheck`, `npm test`, `npm run build` и targeted backend tests;
полный HA harness остаётся каноническим Linux CI. Golden, smoke и performance
не требуются до команды на бету, поскольку визуальный/frontend output не
меняется.
## План автотестов
1. Добавить helper fixture с image record и отсутствующим source asset.
2. Параметризовать `kind=full`, `kind=space` и plan-only variant; создавать
документ через `create_export()`, не конструировать только вручную.
3. Для полного пути пропустить JSON через `create_preview()` на отдельном
пустом target root и проверить content state, confirmation и сохранённый
candidate payload.
4. Создать exact target blob без sidecar и доказать `available`; затем заменить
bytes и доказать `missing_preserved`.
5. Параметризовать `exists_at_export` и `mime` по таблице, включая Python
`False` отдельно от `0`, потому что `bool` — подкласс `int`.
6. Не заменять существующий тест #51: он остаётся регрессией для known MIME при
missing target.
## Release-артефакты
- В `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` — одна парная запись о том,
что повторно экспортированный план с отсутствующей пользовательской картинкой
снова импортируется с подтверждением и без потери объекта.
- В обоих User Guide — короткое уточнение рядом с missing image/import rules.
- В `docs/CONFIG-COMPATIBILITY.md` — нормативная truth table в компактной форме.
- Screenshots/golden не обновляются: видимый рендер и UI не меняются.
- Release/version/tag не входят в задачу; issue остаётся открытой в S8 до беты.
## Риски
1. **Слишком широкое ослабление manifest.** Снимается точной проверкой
`type(exists_at_export) is bool`, белым списком непустого MIME и неизменной
exact identity/hash validation.
2. **Python принимает `0` как `False`.** Проверка должна быть по типу и identity,
а тест содержит `0` и `1` как отрицательные значения.
3. **Source metadata становится authority.** Нельзя использовать supplied MIME
или availability для выбора target файла; target hash проверяется как раньше.
4. **Проверен helper, но не настоящий export/import.** AC1 и AC3 требуют
документы от `create_export()` и хотя бы один путь через `create_preview()`.
## Откат
Откат — revert implementation commit: importer снова потребует supported MIME
у каждой строки `decor_asset`. Данных и миграций откатывать не нужно; уже
импортированные image records остаются валидными по схеме #51. Цена отката —
возврат исходной невозможности импортировать собственный export с missing asset.
## Принятые предположения
- `exists_at_export: false` означает только подтверждённое exporter-ом отсутствие
verified source blob; оно не обещает отсутствие exact blob на target.
- `mime: null` — единственное корректное представление неизвестного MIME,
которое пишет текущий exporter; отсутствие ключа принимается эквивалентно
только в той же строго missing-ветке для устойчивости JSON producers.
- Восстановимый MIME нельзя получить из SHA-256 `asset_id` без blob/sidecar,
поэтому исправляется importer, а не вводится недостоверное значение exporter-а.