Волна 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
32 KiB
ТЗ #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 внешне остаются прежними, но охраняются явными командами и исполнимыми поведенческими тестами.
Проблема и подтверждённые причины
AssetIntegrityVerifier.verify()строит сигнатуру через обычныйstat()и передаёт путь_stream_sha256(). FIFO/устройство проходят эту границу, а blockingopen("rb")может не вернуться. Followers того же single-flight безусловно ждутevent.wait().- Room
pointermoveприshow_room_tooltip: falseвозвращается раньше_showTip(), то есть раньше пути, который вызывает_notePointer().pointerenterне покрывает смену pointer type внутри уже занятой комнаты. - Getter
_dangerConfirmLocaleGateвызывает mutatinglanguageRenderGate(): меняетinert,aria-busy,lang, WeakSet-состояние, запускает lazy load иrequestUpdate(). Его побочный эффект нужен текущему переходу warm→ready до render, но имя и форма скрывают эту обязанность. physical_asset_blobs()не изолируетOSErrorмеждуiterdir()иstat(follow_symlinks=False), аphysical_asset_usage()повторно делаетstat()без защиты._store()затем отображает любойDecorAssetErrorв HTTP 507, хотя 507 означает толькоcapacity_exceeded;invalid_imageдолжен оставаться клиентским отказом 400.- Пять assertions в
test/space-card-audit-lows.test.mjsпроверяют текст исходника. Большая часть соответствующего контракта уже покрытаconfig-storeunit,decor-assetsunit иsmoke_space_card_decor_capability, но source-regex остаётся ложной гарантией и лишней чувствительностью к рефакторингу. pytest.importorskip("homeassistant")на уровнеtest_coordinate_canonicalization.pyскрывает чистые проверкиDECOR_BOX_KINDSи numeric canonicalization в среде без HA. Поэтому мутантimage-box-python-canonicalization-omittedможет завершиться зелёным skip.- #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→ прежний 500io_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 500io_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— 500io_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 со
снятой защитой.
План автотестов
- Расширить verifier tests обычным файлом, missing/directory/non-regular, stable/changed signature, owner/follower и истёкшим follower wait. Реальный FIFO создавать только на POSIX; cross-platform seam доказывает отсутствие вызова hasher независимо от платформы.
- В browser harness заранее создать mouse-owned room tip/fill, выключить
tooltip и отправить pen/touch
pointermoveбез новогоpointerenter. Отдельными spies доказать отсутствие room metric work. - Сохранить полную locale branch matrix, но вызывать публично наблюдаемое действие, а не читать mutating getter ради продвижения состояния.
- Подменить inventory entry/stat так, чтобы один кандидат исчез, а следующий regular blob сохранил count/bytes. Через upload view проверить три status класса без реальной нехватки диска.
- Сопоставить каждую из пяти удаляемых regex-строк с существующим assertion; добавить только отсутствующий capability-only case и mutation, затем удалить source-form block.
- Перенести чистые Python tests без дублирования, запустить новый файл в
окружении без HA и перенаправить mutation guards
decor_box_catalog...иall_4801_lattice_nodes...на него. - На 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.