Files
houseplan-card/docs/specs/295-preflight-diagnostics.md
T
2026-08-26 10:28:28 +03:00

99 lines
11 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
Статус: ревизия 1.
База: #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` в отчёте
`OptimizeSpaceGeometryCheck` получает `detail?: string`: catch-блоки prepare/wall/floor кладут `String((error as Error)?.message ?? error).slice(0, 200)`; для не-exception причин (`wall-null`, `wall-degraded-extra`, `wall-failed-core`, `floor-null`) detail не заполняется (тип причины самодостаточен). Публичная форма результата в остальном не меняется; существующие потребители не ломаются (поле опциональное).
### 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": "<message или null>",
"spaceGeometryFingerprint": "<spacePhysicalGeometryFingerprint(raw)>"
}]
}
```
`origin: runtime` — явное признание AC4: отказ зависит от состояния карточки, экспорт его может не воспроизводить; блок несёт то, чего в экспорте нет (отпечатки, момент, версия). Тот же объект уходит в `console.warn('[houseplan] optimize preflight failed', block)` в ЕДИНОЙ точке — хелпер `_reportPreflightFailure(preflight)` вызывается из обоих мест вычисления не-ok результата (`:15860` и повторная проверка `:15891`).
### 1.5 Условный совет «обновите»
`gs.preflight_update_hint` показывается только если `версия карточки !== версия интеграции` (доступно из hass-конфига интеграции/manifest; если сравнение недоступно — строка не показывается). На актуальном фронте совета нет.
## 2. Скоуп и не-скоуп
**Скоуп:** perечисленное в §1; юниты 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** — существующий тест паритета ловит.
## 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, ни модели, ни миграций.