From 114dcec27cd7404501c3319e55c3842e3961a277 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 26 Aug 2026 09:07:16 +0300 Subject: [PATCH] docs: spec for #295 preflight failure diagnostics Issue: #295 User-Visible: no --- docs/specs/295-preflight-diagnostics.md | 98 +++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/specs/295-preflight-diagnostics.md diff --git a/docs/specs/295-preflight-diagnostics.md b/docs/specs/295-preflight-diagnostics.md new file mode 100644 index 00000000..74d66afd --- /dev/null +++ b/docs/specs/295-preflight-diagnostics.md @@ -0,0 +1,98 @@ +# ТЗ #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) — фолбэк: блок показывается в диалоге текстом (`
` с `
`), чтобы скопировать вручную.
+
+### 1.4 Диагностический блок (единый формат для буфера и dev-лога)
+
+```json
+{
+  "kind": "houseplan-optimize-preflight",
+  "origin": "runtime",
+  "cardVersion": "<версия>",
+  "checkedAt": "",
+  "preflightFingerprint": "",
+  "failures": [{
+    "spaceId": "…", "displayName": "…", "reason": "wall-exception",
+    "detail": "",
+    "spaceGeometryFingerprint": ""
+  }]
+}
+```
+
+`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** — фолбэк `
` обязателен и покрывается смоком (мок 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 — блок в `
` (смок). +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, ни модели, ни миграций.