Files
houseplan-card/legacy/specs/440-v171-beta2-polish.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 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
2026-09-27 22:10:46 +00:00

32 KiB
Raw Permalink Blame History

ТЗ #440 — Полиш аудита v1.71.0-beta.2

  • Issue: https://github.com/Matysh/houseplan-card/issues/440
  • Приоритет: P2, bug / polish / tests
  • Маршрут: full; задача затрагивает несколько независимых TypeScript- и Python-поверхностей, touch-контракт View и защитные гейты, поэтому нарушает ограничения лёгкого трека по сложности и одной поверхности
  • Связанные контракты: #372 (compact static card), #426 (room tooltip toggle), #429/#430 (инфраструктурные гейты), #431 (канонизация image decor), #432 (asset integrity/resolve), #434 (предыдущий audit polish)

Сценарий

Home admin открывает план мышью, пером или на touch-устройстве, меняет язык, подтверждает опасные действия и загружает либо показывает пользовательские изображения подложки. В редком повреждённом состоянии asset-каталога вместо обычного файла может оказаться специальный filesystem object либо запись может исчезнуть во время inventory. Разработчик при этом ожидает, что защитные тесты исполняются и действительно замечают снятую гарантию как локально, так и в CI.

Что человек увидит до и после

До: специальный файл с допустимым asset-именем может навсегда занять поток Home Assistant при HTTP-проверке; исчезнувшая во время inventory запись даёт 500, а повреждённый orphan ошибочно выглядит как нехватка места с HTTP 507. При выключенной подсказке комнаты смена мыши на pen/touch внутри уже наведённой комнаты может оставить transient hover. Остальные пункты сегодня внешне работают, но держатся на неявном побочном эффекте getter либо слабом/CI-only свидетеле.

После: необычные и исчезнувшие asset entries быстро и безопасно отбрасываются, код HTTP соответствует причине ошибки, а реальный pen/touch всегда снимает mouse-only hover независимо от настройки room tooltip. Языковые переходы и static-card capability внешне остаются прежними, но охраняются явными командами и исполнимыми поведенческими тестами.

Проблема и подтверждённые причины

  1. AssetIntegrityVerifier.verify() строит сигнатуру через обычный stat() и передаёт путь _stream_sha256(). FIFO/устройство проходят эту границу, а blocking open("rb") может не вернуться. Followers того же single-flight безусловно ждут event.wait().
  2. Room pointermove при show_room_tooltip: false возвращается раньше _showTip(), то есть раньше пути, который вызывает _notePointer(). pointerenter не покрывает смену pointer type внутри уже занятой комнаты.
  3. Getter _dangerConfirmLocaleGate вызывает mutating languageRenderGate(): меняет inert, aria-busy, lang, WeakSet-состояние, запускает lazy load и requestUpdate(). Его побочный эффект нужен текущему переходу warm→ready до render, но имя и форма скрывают эту обязанность.
  4. physical_asset_blobs() не изолирует OSError между iterdir() и stat(follow_symlinks=False), а physical_asset_usage() повторно делает stat() без защиты. _store() затем отображает любой DecorAssetError в HTTP 507, хотя 507 означает только capacity_exceeded; invalid_image должен оставаться клиентским отказом 400.
  5. Пять assertions в test/space-card-audit-lows.test.mjs проверяют текст исходника. Большая часть соответствующего контракта уже покрыта config-store unit, decor-assets unit и smoke_space_card_decor_capability, но source-regex остаётся ложной гарантией и лишней чувствительностью к рефакторингу.
  6. pytest.importorskip("homeassistant") на уровне test_coordinate_canonicalization.py скрывает чистые проверки DECOR_BOX_KINDS и numeric canonicalization в среде без HA. Поэтому мутант image-box-python-canonicalization-omitted может завершиться зелёным skip.
  7. #429 и #430 намеренно были чистыми инфраструктурными задачами без файлов класса A. PROCESS.md §1 исключает для них ТЗ и код-ревью; подробные closing-комментарии и коммиты уже являются корректным следом. Это не дефект реализации и не требует синтетического review-документа задним числом.

Скоуп

В скоупе:

  • fail-fast regular-file boundary перед digest/cache/single-flight публикацией;
  • конечное ожидание followers как дополнительная защита liveness;
  • сохранение fail-dark HTTP 404 для отсутствующего, изменившегося, специального либо неподтвердившегося asset blob;
  • обновление pointer modality на каждом реальном room pointermove в View, включая выключенный tooltip;
  • явная command/query граница для danger-confirm locale state при полном сохранении ready/cold/warm поведения #434;
  • устойчивый к исчезновению файлов physical asset inventory;
  • точное отображение store errors: capacity → 507, invalid image/прочий корректный клиентский отказ → 400, OSError → прежний 500 io_error;
  • замена source-regex AC5 #434 поведенческими unit/smoke-свидетелями;
  • HA-независимый модуль чистых Python canonicalization-тестов и перевод соответствующих mutation guards на него;
  • тесты, mutation witnesses, техническая документация и changelog.

Не-скоуп

  • новый asset format, protocol capability, digest, quota либо filesystem watcher;
  • поддержка FIFO/devices/sockets как изображений или попытка читать их с timeout;
  • автоматическое удаление необычных/orphan entries;
  • изменение прав доступа, signed URL, catalog/list/resolve semantics или content-addressed имени;
  • новый room tooltip, отдельный pen UX либо изменение touch-first контракта;
  • новый вид/текст danger confirmation или изменение fallback-языка;
  • рефакторинг всего LanguageRuntime либо всех Lit render gates;
  • изменение канонизации координат, schema/storage результата #431;
  • создание review-документов задним числом для #429/#430;
  • golden, performance baseline или полный Windows HA harness.

Контракт поведения

1. Целостность asset и liveness

Verifier принимает к хешированию только существующий обычный файл. Directory, FIFO, socket, block/character device, broken link и исчезнувший путь дают false до вызова digest hasher и не создают доверенную cache entry.

Проверка типа выполняется как часть обеих сигнатур — до и после чтения. Digest публикуется только если один и тот же обычный file version оставался стабильным по size/mtime/ctime. Ошибка либо смена типа очищает stale cache для canonical path и будит followers с false.

Follower не ждёт бесконечно даже при неожиданно зависшем/injected owner: ожидание имеет конечную внутреннюю границу и при её истечении возвращает false, не удаляя чужой in-flight record и не публикуя digest. Значение границы техническое; оно должно существенно превышать чтение разрешённого asset максимального размера и не является пользовательским network timeout.

HTTP content endpoint сохраняет прежнее поведение: verifier false означает 404 без передачи файловых деталей клиенту. Обычные корректные assets и LRU single-flight/cache работают как раньше.

2. Pointer modality при выключенной подсказке комнаты

Каждый настоящий room pointermove в View сначала сообщает pointer type общей instance-local modality, а уже потом решает, показывать ли room tooltip.

  • mouse сохраняет существующие room fill/hover и tooltip rules;
  • touch или pen снимает _tip и _hoverRoom, которые принадлежат mouse-hover, даже при show_room_tooltip: false;
  • выключенная подсказка не считает area/temp/humidity/LQI и не создаёт tooltip;
  • editor modes и device/opening tips не получают нового UX.

Повторный вызов modality helper на пути включённого tooltip не должен менять наблюдаемое поведение или порождать лишнее состояние.

3. Явная locale command для danger confirmation

Mutating синхронизация языка не маскируется getter-ом. Call site, которому нужно актуализировать inert/attributes/lazy load и получить gate, вызывает явно названную command-функцию/метод; чистое чтение уже вычисленного значения, если потребуется, не имеет побочных эффектов.

Сохраняются все ветви #434:

  • ready/fallback до следующего render немедленно разрешает обычный request и снимает inert/aria-busy;
  • warm немедленно возвращает false, не оставляет controller request;
  • ready→warm отменяет уже открытое подтверждение до потери decision source;
  • cold first frame остаётся language-neutral loading surface;
  • stable body в warm не заменяется, согласие между языковыми состояниями не переносится.

Ни render, ни willUpdate, ни _confirmDanger() не читают property, похожее на обычный getter, если это чтение меняет DOM/host/runtime state.

4. Inventory race и HTTP status

Physical inventory обходит только exact allowed-extension/hash candidates и учитывает только успешно наблюдённые обычные файлы. Если root либо отдельная entry исчезла, стала недоступна или сменила тип между обходом и stat, этот кандидат пропускается, остальные продолжают считаться. Одна race не превращает весь upload в io_error.

Семантика quota остаётся консервативной в пределах одного наблюдения: count и bytes относятся к тем же успешно stat-нутым regular candidates; sidecar по- прежнему не является authority physical usage.

Ответ upload после _store():

  • DecorAssetError("capacity_exceeded") → HTTP 507 с тем же error code;
  • DecorAssetError("invalid_image") и иные не-capacity validation/integrity коды → HTTP 400 с исходным code/message;
  • неожиданный OSError → HTTP 500 io_error;
  • успех/reused и pre-store too_large/format behavior не меняются.

5. Поведенческие witnesses static-card capability

Source-regex блок #434 удаляется как гарантия. Его контракт доказывается через публично наблюдаемые данные и вызовы:

  • fresh config/get принимает только exact API v1 и отзывает capability при missing/другом значении;
  • localStorage snapshot никогда не выдаёт runtime capability;
  • static card без v1 не вызывает resolve и очищает runtime asset map;
  • capability-only upgrade/downgrade принимается при том же config body;
  • resolve cache разделён authoritative revision/epoch и повторяет прежний missing на новом epoch.

Если существующие unit/smoke уже доказывают строку полностью, они расширяются только недостающим отрицательным кейсом; дублирующий regex не сохраняется. Mutation/negative run обязан краснеть на наблюдаемом счётчике вызовов, snapshot либо map, а не на изменении форматирования исходника.

6. Чистая Python-канонизация исполняется без HA

Тесты, которым нужны только custom_components.houseplan.coordinate_canonicalization и JSON fixture, живут в отдельном модуле до любой HA-зависимой загрузки. Как минимум туда переходят:

  • общий scalar/lattice fixture contract;
  • exact DECOR_BOX_KINDS и image box field preservation;
  • scalar symmetry/off-grid behavior;
  • 4801 lattice nodes и nine-decimal forms.

Schema, store, virtual lights и wall segment tests остаются в HA-зависимом модуле под честным importorskip. Mutation guards для чистых Python contracts указывают новый модуль и в среде без Home Assistant обязаны давать реальный pass/fail, а не module skip. Дублировать одни assertions в обоих файлах нельзя.

7. Пункт #429/#430

Никаких продуктовых либо repository-правок ради создания отсутствовавшего review artifact не делается. ТЗ и итоговый комментарий #440 фиксируют проверку: обе задачи были infrastructure-only и прошли допустимый §1 маршрут. Если выяснится, что их коммиты всё же содержали класс A, это новая process issue, а не скрытое расширение #440.

Модель данных, совместимость и i18n

  • Persisted config/layout, schema version, asset sidecar и API capability не меняются; миграции нет.
  • HTTP status исправляется без изменения JSON error/message формы.
  • Новых пользовательских строк нет; словари en/ru/de/fr не меняются.
  • Старые frontend/backend combinations сохраняют действующие fail-closed capability и content URL contracts.

Touch и доступность

View остаётся touch-first. Исправление pointer modality восстанавливает обязательный контракт docs/TOUCH-SUPPORT.md: touch/pen не наследует визуальное состояние mouse hover. Никаких новых жестов, click targets или editor touch обещаний нет.

Danger-confirm сохраняет alertdialog, focus, inert и aria-busy semantics; меняется только явность точки, где эти эффекты синхронизируются.

Производительность и безопасность

  • Regular-file check добавляет bounded stat/type verification вокруг уже существующего SHA-256 и не расширяет число хеширований/cache entries.
  • Конечное follower wait не удерживает executor бесконечно; обычный cache hit и single-flight путь остаются O(1).
  • Inventory остаётся O(entries) под существующим upload_lock, quota физически ограничивает каталог; исчезнувшая entry не останавливает весь scan.
  • Необычный файл никогда не читается и не становится trusted asset. Ошибки не раскрывают filesystem path/type.
  • Pointer и locale изменения не добавляют работу в стабильный кадр сверх уже существующего modality/gate вызова; это подтверждается review кода, нового performance baseline не требуется.

Затронутые модули

Ожидаемый набор; точное выделение helpers может быть скорректировано ревьюером:

  • custom_components/houseplan/asset_integrity.py;
  • custom_components/houseplan/decor_assets.py;
  • custom_components/houseplan/http_api.py;
  • src/houseplan-card.ts, при необходимости src/i18n/language-runtime.ts;
  • tests_backend/test_decor_assets.py, новый чистый Python test module и соответствующие HA endpoint tests;
  • test/space-card-audit-lows.test.mjs, test/config-store.test.mjs, test/decor-assets.test.mjs;
  • demo/smoke_room_tooltip_toggle.mjs либо отдельный pointer-modality smoke, demo/smoke_danger_confirm_branches.mjs, demo/smoke_space_card_decor_capability.mjs;
  • scripts/mutation-gate.mjs;
  • docs/ARCHITECTURE.md, docs/TESTING.md, оба changelog и docs screenshot fingerprint, если их действующие разделы требуют обновления.

Критерии приёмки

  • AC1 (backend/unit, safety/liveness). Обычный asset с верным digest проверяется и кэшируется; directory/FIFO/другой non-regular и исчезнувший path возвращают false без вызова hasher. Followers получают тот же результат и конечный wait не зависает. Смена bytes/type между сигнатурами не публикует cache. Доказательство: pure verifier matrix, POSIX FIFO case под platform-skip и отдельный injected timeout/flight case.
  • AC2 (browser smoke, touch). При выключенном room tooltip переход реального pointermove mouse→pen и mouse→touch внутри комнаты снимает _tip и _hoverRoom, не создаёт новый tooltip и не вычисляет room metrics; mouse и включённый tooltip остаются прежними. Доказательство: targeted browser smoke с pointer events и metric spies.
  • AC3 (browser smoke + review, lifecycle). Danger locale sync вызывается явной command-границей, getter с mutating languageRenderGate() отсутствует; ready→warm, warm refusal, warm→ready до render, cancellation, stable body и inert/aria-busy проходят прежнюю полную матрицу. Доказательство: smoke_danger_confirm_branches + negative mutation, diff review command/query call sites.
  • AC4 (backend/unit + HA endpoint, correctness). Исчезновение/type-change одного inventory candidate не останавливает scan и не искажает остальные count/bytes. Capacity store error отвечает 507, invalid orphan image — 400, OSError — 500 io_error; body code сохраняется. Доказательство: filesystem race matrix + upload response matrix.
  • AC5 (unit/smoke, compatibility). Все пять static-card capability/cache обещаний #434 доказаны значениями snapshot/map и WS counters; source-regex для этого блока удалён. Снятие exact capability revocation/adoption краснит поведенческий тест. Доказательство: config/decor units, smoke_space_card_decor_capability и targeted mutation.
  • AC6 (backend/unit + mutation, harness). Чистые coordinate/image canonicalization tests выполняются без установленного homeassistant; HA- dependent tests по-прежнему честно skip-аются локально и исполняются в Linux CI. image-box-python-canonicalization-omitted и чистый lattice mutant краснеют на новом модуле. Доказательство: pytest в среде без HA, backend-test-guard и оба mutation ids.
  • AC7 (review/docs, scope). #429/#430 не получают ретроактивных artifacts; нет schema/API/i18n/устойчивого визуального изменения. Обычная раздача assets, upload success/reuse, tooltip-on и language fallback остаются совместимы. Доказательство: diff review, существующие contract tests и документация.
  • AC8 (gates). Typecheck, unit, build/bundle sync, выбранные browser smokes, чистые backend tests, Linux HA CI, docs checks и новые mutation witnesses зелёные на exact SHA. Golden и full performance остаются предрелизными.

Таблица защитных доказательств

AC Чем доказан Чем обязан покраснеть
AC1 verifier regular/non-regular/single-flight matrix снятие regular-file boundary вызывает hasher на sentinel/FIFO; снятие wait boundary оставляет controlled follower unresolved
AC2 room pointer-modality browser smoke перенос _notePointer обратно после tooltip guard оставляет _hoverRoom/_tip после pen/touch
AC3 danger locale branch smoke возврат mutating getter/cached прошлого render даёт ложный warm отказ либо оставляет inert/controller в переходе
AC4 inventory race + upload status backend tests проброс OSError снова делает 500; общий status 507 заставляет invalid-image assertion получить неверный статус
AC5 config-store/decor units + static-card WS smoke снятие exact capability adoption/revocation вызывает resolve на старом backend либо оставляет asset map после downgrade
AC6 HA-free pytest module + mutation guards удаление image из Python DECOR_BOX_KINDS либо изменение lattice rounding даёт failed assertion, не skipped module

Для AC1–AC6, заявляющих защиту, code-review обязан привести точный negative run по правилу PROCESS.md §2.7. Persistent mutation нужен для дорогого browser/backend witness; для чистого unit допустим документированный red-run со снятой защитой.

План автотестов

  1. Расширить verifier tests обычным файлом, missing/directory/non-regular, stable/changed signature, owner/follower и истёкшим follower wait. Реальный FIFO создавать только на POSIX; cross-platform seam доказывает отсутствие вызова hasher независимо от платформы.
  2. В browser harness заранее создать mouse-owned room tip/fill, выключить tooltip и отправить pen/touch pointermove без нового pointerenter. Отдельными spies доказать отсутствие room metric work.
  3. Сохранить полную locale branch matrix, но вызывать публично наблюдаемое действие, а не читать mutating getter ради продвижения состояния.
  4. Подменить inventory entry/stat так, чтобы один кандидат исчез, а следующий regular blob сохранил count/bytes. Через upload view проверить три status класса без реальной нехватки диска.
  5. Сопоставить каждую из пяти удаляемых regex-строк с существующим assertion; добавить только отсутствующий capability-only case и mutation, затем удалить source-form block.
  6. Перенести чистые Python tests без дублирования, запустить новый файл в окружении без HA и перенаправить mutation guards decor_box_catalog... и all_4801_lattice_nodes... на него.
  7. На implementation SHA прогнать штатные гейты и каждый новый/перенесённый negative witness; failure должен быть целевым assertion, не import/skip, timeout всего процесса или отсутствующим браузером.

Риски

  • Regular check не закрывает race между path stat и open. Смягчение: post-read signature/type guard и fail-dark результат; реализация вправе использовать безопасный file-descriptor seam, если это не ломает injected hasher tests и Windows.
  • Слишком короткий follower timeout даст ложные 404 на медленном диске. Смягчение: техническая граница существенно выше чтения максимальных 2 MiB, обычный owner продолжает и может заполнить cache для следующего запроса.
  • Пропуск исчезнувшей entry временно недосчитает quota. Это внешний race в config-каталоге; House Plan writers уже сериализованы upload_lock, а следующий запрос пересканирует store. Нельзя превращать один race в отказ всех корректных upload.
  • Locale refactor изменит тонкое pre-render окно. Смягчение: существующий smoke сохраняется как основной acceptance witness и получает negative proof.
  • Удаление regex оставит дыру. Смягчение: таблица соответствия пяти строк наблюдаемым unit/smoke assertions до удаления и mutation по поведению.
  • Перенос Python tests изменит CI collection. Смягчение: отсутствие дубликатов, отдельные прогоны с/без HA и существующий harness audit.

Откат

Откат — единый revert implementation commit вместе с тестами и release- артефактами. Persisted data и migration отсутствуют. Если потребуется частичный аварийный откат, backend regular-file/status fixes и frontend pointer/locale fix могут быть возвращены независимыми атомарными коммитами только вместе со своими свидетелями; возврат зависающего FIFO либо ложного 507 не считается безопасной деградацией.

Release-артефакты

  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md: fail-fast asset verification, честные upload errors и восстановление pointer modality;
  • docs/ARCHITECTURE.md: обновить только если описание asset integrity не фиксирует regular-file/fail-dark границу;
  • docs/TESTING.md: HA-free canonicalization witnesses и отказ от source-regex как семантического доказательства;
  • docs screenshot workflow: подтвердить нулевую pixel-дельту и принять только source fingerprint после src/**;
  • user guide, i18n, golden baselines, performance/security profiles и schema docs: без изменений;
  • implementation commit — User-Visible: yes, обе записи changelog в том же коммите; чистый follow-up fingerprint/review doc — User-Visible: no.

Принятые технические предположения

Эти решения не меняют продуктовый замысел и могут быть свободно скорректированы ревьюером до S5:

  • задача остаётся единым audit batch: пункты малы, имеют общую цель hardening и требуют одного согласованного набора negative witnesses;
  • follower timeout — внутренняя liveness-защита, а не обещание времени HTTP- ответа; точное число выбирается по текущему 2 MiB лимиту и тестируется через injected event, без реального ожидания;
  • non-capacity DecorAssetError после _store() отображается в 400, потому что его code описывает непринимаемый asset, а не состояние диска;
  • mutating language sync может оставаться вызываемой из Lit lifecycle и action path, но обязана называться командой и не притворяться property read;
  • существующие поведенческие tests переиспользуются вместо создания второго параллельного harness, если они полностью доказывают AC;
  • ссылки на номера строк из аудита ориентировочны: реализация привязывается к символам и поведению актуального origin/dev.