Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в `legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код, тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ — на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`), DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди перенесённых нет. Относительные ссылки перенесённых файлов переписаны (`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) — все 26 резолвятся. Попутно: битая ссылка в `089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` — теперь команда `git show` по истории. Строка в `legacy/README.md`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
13 KiB
ТЗ #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-лога)
{
"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. Риски
- Clipboard в HA-вебвью/insecure context — фолбэк
<details><pre>обязателен и покрывается смоком (мок writeText, бросающий исключение). - Рост шума dev-лога — лог пишется один раз на вычисление не-ok preflight (не на рендер) — хелпер с дедупликацией по fingerprint.
- Паритет i18n — существующий тест паритета ловит.
- Производительность — не затрагивается (L2): диагностика строится только в отказной ветке уже вычисленного preflight; ok-путь не получает ни одной новой операции; dev-лог дедуплицирован по fingerprint.
5. AC (сведены с issue)
- Отказ показывает причину и пространство: для каждого из 7 reason строка в диалоге (юнит: маппинг reason→ключ полный; смок: фикстура с ломаной геометрией → текст причины виден).
- Dev-лог несёт полную запись (spaceId, reason, detail, отпечатки) — смок перехватывает console.warn.
- Кнопка копирует блок §1.4 (смок: мок clipboard, сверка JSON), фолбэк при отказе clipboard — блок в
<details>(смок). - Блок называет рантайм-природу:
origin: runtime,checkedAt, отпечатки — есть в JSON (юнит/смок). - Тест невалидной геометрии: сцена с заведомо ломаной геометрией даёт
failedс конкретной причиной, доходящей до текста диалога (смок, падает на текущем dev — сейчас в диалоге причин нет). - «Обновите House Plan» не показывается при совпадающих версиях; показывается при расхождении (юнит на предикат + смок одной из веток).
- Мутанты: (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, ни модели, ни миграций.