From 735710f16b8f80772a74a73a2547e32effc3b367 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Thu, 3 Sep 2026 16:36:06 +0300 Subject: [PATCH] docs: specify v1.71 beta 2 audit polish Issue: #440 User-Visible: no --- docs/specs/440-v171-beta2-polish.md | 422 ++++++++++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 423 insertions(+) create mode 100644 docs/specs/440-v171-beta2-polish.md diff --git a/docs/specs/440-v171-beta2-polish.md b/docs/specs/440-v171-beta2-polish.md new file mode 100644 index 00000000..0de292e2 --- /dev/null +++ b/docs/specs/440-v171-beta2-polish.md @@ -0,0 +1,422 @@ +# ТЗ #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`. diff --git a/docs/specs/README.md b/docs/specs/README.md index abcd4e50..e8d1b3d1 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -168,6 +168,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#426](https://github.com/Matysh/houseplan-card/issues/426) Отключение информационного окна комнаты при наведении | [426-room-hover-tooltip-toggle.md](426-room-hover-tooltip-toggle.md) | | [#431](https://github.com/Matysh/houseplan-card/issues/431) Канонизация координат пользовательских изображений | [431-image-coordinate-canonicalization.md](431-image-coordinate-canonicalization.md) | | [#432](https://github.com/Matysh/houseplan-card/issues/432) Ограниченный resolve и единая проверка целостности изображений | [432-asset-resolve-authorization-cache.md](432-asset-resolve-authorization-cache.md) | +| [#440](https://github.com/Matysh/houseplan-card/issues/440) Полиш аудита v1.71.0-beta.2 | [440-v171-beta2-polish.md](440-v171-beta2-polish.md) | ## P3