Files
houseplan-card/docs/specs/440-v171-beta2-polish.md
T
2026-09-03 16:36:06 +03:00

423 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ #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`.