mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 03:38:47 +00:00
100 lines
13 KiB
Markdown
100 lines
13 KiB
Markdown
# ТЗ #295 — Отказ геометрического preflight называет причину и отдаёт диагностику
|
||
|
||
Issue: https://github.com/Matysh/houseplan-card/issues/295
|
||
Статус: ревизия 2 (после SPEC-REVIEW-295-r1: H1 — граница приватности #199 сохраняется, detail = класс исключения; M1 — integration_version добавляется в houseplan/config/get; L1/L2).
|
||
База: #199 (fail-closed барьер — работает и не меняется), #291 (решётка), #265 (непроверенные позиции — воспроизводимая часть отчёта владельца).
|
||
|
||
## 0. Сценарий
|
||
|
||
Владелец жмёт «Оптимизировать», барьер #199 отклоняет плохой кандидат — и сообщает только имена пространств с советом «Обновите House Plan», которому на rc некуда следовать. Причина вычислена (7 значений `OptimizeGeometryFailureReason`) и выброшена; экспорт отказ не воспроизводит, потому что отказ рантаймовый. Дефект недиагностируем — ни владелец, ни разработчик не могут сделать следующий шаг.
|
||
|
||
**До:** «Не удалось безопасно проверить геометрию: 2 этаж. Обновите House Plan и повторите.»
|
||
**После:** по каждому пространству — человеческая причина («Тело стен не построилось: деградация с лишней геометрией», …), кнопка «Скопировать диагностику» кладёт в буфер машинный блок (версия, отпечатки, причины, detail исключений, пометка origin: runtime), тот же блок уходит в dev-лог; совет «обновите» показывается только при реально устаревшем фронте.
|
||
|
||
## 1. Контракт
|
||
|
||
### 1.1 `plan-geometry-preflight.ts`: `detail` в отчёте — граница #199 сохраняется
|
||
|
||
Барьер #199 документированно запрещает выпуск текста исключений из `checkOptimizeGeometry` («Geometry values never escape this call… not polygon output or exception text», plan-geometry-preflight.ts:312, docs/CANVAS.md, docs/ARCHITECTURE.md; три `assert.doesNotMatch` в тестах). Решение H1 — вариант (а) с усилением: **raw `error.message` не выносится никуда**. `OptimizeSpaceGeometryCheck` получает `detail?: string` = **класс исключения** (`error.name`, для не-Error — `typeof`), только для `*-exception` причин; для остальных причин поле не задаётся (остаётся `undefined`, тип самодостаточен). Класс — это тип, не содержимое: приватная граница не расширяется, три privacy-теста остаются и дополняются позитивной проверкой `detail === 'Error'`. Docs #199 не требуют правки — «exception text» по-прежнему не покидает вызов. Публичная форма результата в остальном не меняется.
|
||
|
||
### 1.2 i18n: причины и действия
|
||
|
||
Новые ключи RU+EN (паритет обязателен):
|
||
|
||
- `gs.preflight_reason_prepare-exception` — «Не удалось подготовить геометрию пространства (исключение при сборке модели)»;
|
||
- `…_wall-null` — «Тело стен не построилось (union вернул пустоту)»;
|
||
- `…_wall-degraded-extra` — «Тело стен деградировало с лишней геометрией»;
|
||
- `…_wall-failed-core` — «Ядро тела стен не собралось»;
|
||
- `…_wall-exception` — «Построение стен упало с исключением»;
|
||
- `…_floor-null` — «Контур пола не построился»;
|
||
- `…_floor-exception` — «Построение пола упало с исключением»;
|
||
- `gs.preflight_copy` — «Скопировать диагностику», `gs.preflight_copied` — «Диагностика скопирована»;
|
||
- `gs.align_preflight_hint` переписывается: «Планы не изменены. Скопируйте диагностику и приложите её к отчёту вместе с экспортом пространства.» Совет «Обновите House Plan и повторите» переносится в условную строку `gs.preflight_update_hint`, которая показывается ТОЛЬКО когда версия карточки отличается от версии интеграции (см. §1.5).
|
||
|
||
EN-формулировки — прямые переводы. Тексты выше — предложение; финальную русскую редакцию можно поправить на ревью ТЗ без смены структуры.
|
||
|
||
### 1.3 Диалог «Оптимизировать» (ветка failed)
|
||
|
||
Вместо одной строки с именами: список `preflight.failures` — `displayName: причина` (по ключам §1.2), максимум 10 строк + «и ещё N». Под списком — кнопка `gs.preflight_copy`; по клику `navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2))`, тост `gs.preflight_copied`. Ошибка clipboard (нет прав/insecure context) — фолбэк: блок показывается в диалоге текстом (`<details>` с `<pre>`), чтобы скопировать вручную.
|
||
|
||
### 1.4 Диагностический блок (единый формат для буфера и dev-лога)
|
||
|
||
```json
|
||
{
|
||
"kind": "houseplan-optimize-preflight",
|
||
"origin": "runtime",
|
||
"cardVersion": "<версия>",
|
||
"checkedAt": "<ISO>",
|
||
"preflightFingerprint": "<fingerprint из результата>",
|
||
"failures": [{
|
||
"spaceId": "…", "displayName": "…", "reason": "wall-exception",
|
||
"detail": "<класс исключения, например Error/TypeError; null в JSON, если причина не *-exception>",
|
||
"spaceGeometryFingerprint": "<spacePhysicalGeometryFingerprint(raw)>"
|
||
}]
|
||
}
|
||
```
|
||
|
||
В самом результате preflight незаполненный `detail` — `undefined` (поле отсутствует); в JSON-блоке диагностики он сериализуется явным `null` (`failure.detail ?? null`) — это разные слои, формулировка разведена по находке L1. `origin: runtime` — явное признание AC4: отказ зависит от состояния карточки, экспорт его может не воспроизводить; блок несёт то, чего в экспорте нет (отпечатки, момент, версия). Тот же объект уходит в `console.warn('[houseplan] optimize preflight failed', block)` в ЕДИНОЙ точке — хелпер `_reportPreflightFailure(preflight)` вызывается из обоих мест вычисления не-ok результата (`:15860` и повторная проверка `:15891`).
|
||
|
||
### 1.5 Условный совет «обновите»
|
||
|
||
Канала с текущей версией интеграции у фронта нет (находка M1: `houseplan/config/get` её не возвращает; `hass.config` — core-конфиг HA; `integration_version` бэкапа — версия на момент экспорта файла). Поэтому: **бэкенд `websocket_api.py` добавляет `integration_version` в ответ `houseplan/config/get`** (значение — тот же `VERSION`, что кладёт import_export.py:526); карточка запоминает его при загрузке конфига. `gs.preflight_update_hint` показывается только если полученная версия непуста и `!== CARD_VERSION`; при отсутствии поля (старый бэкенд) строка не показывается — отсутствующая подсказка честнее вводящей в заблуждение.
|
||
|
||
## 2. Скоуп и не-скоуп
|
||
|
||
**Скоуп:** перечисленное в §1; `custom_components/houseplan/websocket_api.py` (+ его тест) — `integration_version` в `houseplan/config/get`; юниты preflight на `detail`; смок диалога; мутант на потерю reason; docs (USER-GUIDE раздел «Оптимизировать» — абзац о диагностике; CHANGELOG RU+EN).
|
||
**Не-скоуп:** сам рантайм-отказ владельца (станет диагностируемым — на его данные заводится/дополняется отдельный issue после первого же скопированного блока); механика барьера #199; внерешёточная запись из «побочного наблюдения» (кандидат в отдельный issue); экспортный формат.
|
||
|
||
## 3. UX, данные, i18n, touch
|
||
|
||
UI: расширение существующего диалога (список+кнопка+details-фолбэк) — паттерны кнопок/тостов существующие. Данные/миграции: нет. i18n: ключи §1.2, RU+EN, паритет-тест. Touch: кнопка обычная, жестов нет.
|
||
|
||
## 4. Риски
|
||
|
||
1. **Clipboard в HA-вебвью/insecure context** — фолбэк `<details><pre>` обязателен и покрывается смоком (мок writeText, бросающий исключение).
|
||
2. **Рост шума dev-лога** — лог пишется один раз на вычисление не-ok preflight (не на рендер) — хелпер с дедупликацией по fingerprint.
|
||
3. **Паритет i18n** — существующий тест паритета ловит.
|
||
4. **Производительность** — не затрагивается (L2): диагностика строится только в отказной ветке уже вычисленного preflight; ok-путь не получает ни одной новой операции; dev-лог дедуплицирован по fingerprint.
|
||
|
||
## 5. AC (сведены с issue)
|
||
|
||
1. Отказ показывает причину и пространство: для каждого из 7 reason строка в диалоге (юнит: маппинг reason→ключ полный; смок: фикстура с ломаной геометрией → текст причины виден).
|
||
2. Dev-лог несёт полную запись (spaceId, reason, detail, отпечатки) — смок перехватывает console.warn.
|
||
3. Кнопка копирует блок §1.4 (смок: мок clipboard, сверка JSON), фолбэк при отказе clipboard — блок в `<details>` (смок).
|
||
4. Блок называет рантайм-природу: `origin: runtime`, `checkedAt`, отпечатки — есть в JSON (юнит/смок).
|
||
5. Тест невалидной геометрии: сцена с заведомо ломаной геометрией даёт `failed` с конкретной причиной, доходящей до текста диалога (смок, падает на текущем dev — сейчас в диалоге причин нет).
|
||
6. «Обновите House Plan» не показывается при совпадающих версиях; показывается при расхождении (юнит на предикат + смок одной из веток).
|
||
7. Мутанты: (a) reason теряется на пути в диалог (маппинг подменён на пустую строку) — красный; (b) диагностический блок без `failures[].reason` — красный; (c) dev-лог отключён — красный. Гварды с типовым прологом.
|
||
|
||
## 6. План тестов
|
||
|
||
Юниты: `detail` из catch (все три), маппинг 7 причин, предикат версии. Смоки: диалог с причинами + копирование + фолбэк + console.warn. Мутанты §5.7. Существующие юниты preflight и смоки оптимизации — без изменений поведения ok-пути.
|
||
|
||
## 7. Release-артефакты
|
||
|
||
CHANGELOG RU+EN (User-Visible: yes), USER-GUIDE.ru.md (диагностика в «Оптимизировать»), fingerprint скриншотов.
|
||
|
||
## 8. Откат
|
||
|
||
Один revert: UI/лог/i18n, ни модели, ни миграций.
|