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

11 KiB
Raw Blame History

ТЗ #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-лога)

{
  "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, ни модели, ни миграций.