Files
houseplan-card/docs/specs/295-preflight-diagnostics.md
T

100 lines
13 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.
# ТЗ #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, ни модели, ни миграций.