Issue: #426 Issue: #427 Issue: #428 Issue: #431 Issue: #432 Issue: #434 User-Visible: no
41 KiB
ТЗ #434 — Полиш аудита v1.71.0-beta.1
- Issue: https://github.com/Matysh/houseplan-card/issues/434
- Приоритет: P3,
bug/polish/tests - Маршрут: full; задача меняет Python- и TypeScript-продукт, хранение пользовательских файлов, rolling compatibility, асинхронный safety lifecycle и несколько независимых гейтов, поэтому не проходит лимит сложности и одной поверхности лёгкого трека
- Связанные контракты: #51 (пользовательские изображения), #417 (ветви подтверждения опасных действий), #418/#423 (support preview), #419 (Area snapshot cleanup), #432 (resolve и целостность assets)
Сценарий
Home admin пользуется изображениями в Редакторе подложки и отдельной карточкой пространства, обновляет frontend и интеграцию через HACS, меняет язык либо отправляет обращение в поддержку. В редком аварийном состоянии Home Assistant может остановиться между записью blob и sidecar. Одновременно разработчик рассчитывает, что защитные тесты действительно краснеют при снятии проверок, а один зависший browser smoke не удерживает весь CI job до глобального timeout.
Что человек увидит до и после
До: аварийно оставшийся файл изображения не входит в лимит и не удаляется
явным API-вызовом, повторная загрузка неверно выглядит как переиспользование;
static card шумит ошибкой на старом backend, а восстановленное изображение может
остаться missing до переподключения. В переходе языка подтверждение опасного
действия может зависнуть или быть ошибочно отклонено, а невалидный ответ support
preview удерживает временный слот десять минут.
После: физический store честно учитывает аварийные blobs, повторная загрузка безопасно восстанавливает каталог, явное удаление действительно удаляет точный asset; обе карточки соблюдают capability и revision cache. Подтверждение всегда либо показано, либо немедленно и безопасно отменено, а неиспользованный support token отзывается. Видимые контролы, тексты и обычные успешные сценарии не меняются.
Цель
Закрыть девять подтверждённых разрывов аудита v1.71.0-beta.1 и закрепить для каждой защитной ветки отрицательный свидетель:
- физический учёт и явное удаление orphan decor blobs;
- capability guard в
houseplan-space-card; - cache epoch resolve по ревизии config;
- точный тест наличия blob для catalog row;
- честный
reusedпри восстановлении sidecar; - актуальный locale gate и симметричная отмена danger confirmation;
- свидетель принадлежности кандидата текущему Area snapshot;
- локальные timeout для German wait и каждого CI smoke-файла;
- отзыв валидного support token из непринятого ответа.
Подтверждённые причины
read_catalog()обходит только*.json, а_read_catalog_row()принимает строку лишь при существующем blob. Поэтому blob без корректного sidecar не виден ни каталогу, ни quota, а delete сначала ищет ту же невидимую строку.HpConfigSnapshotне переноситdecor_assets_api, и_load()static card безусловно вызываетhouseplan/assets/resolve.resolveCacheхранит одну парунабор id → Mapна connection и не включает принятую серверную ревизию config;missingтем самым становится бессрочным для прежнего набора id.- Тест с названием
test_catalog_ignores_missing_or_malformed_sidecarsсоздаёт blob для единственной валидной строки и не проверяет обратный случай «валидный sidecar, blob отсутствует». - Upload при уже существующем exact blob дописывает sidecar и возвращает
reused: true, хотя до операции promoted catalog entry не существовала. _confirmDanger()читает_dangerConfirmLocaleGateпрошлого render. Между изменением языка и render это даёт ложное разрешение, а между завершением загрузки языка и render — ложный отказ. Переход вwarm/noChangeне отменяет уже открытый controller request и оставляет его в inert DOM.- Условие
snapshotBindings.has(binding)вresolveAreaSnapshotCleanup()не имеет кейса, в котором все соседние проверки истинны, а binding отсутствует только в текущем snapshot. germanStartedограничен одной секундой, соседнийgermanCompletedожидается без границы; CI запускает каждыйdemo/smoke_*.mjsпростымnode, а сама jobsmokeне имеетtimeout-minutesи потому наследует 360-минутный default GitHub Actions. Единственные 20 минут в workflow относятся к другой job —performance_smoke._buildSupportPreview()извлекает token, но при невалидности другого поля бросаетsupport_rejectedдо_discardSupportPreview().
Скоуп
В скоупе:
- независимый физический inventory promoted decor blobs для quota/delete;
- восстановление missing/broken sidecar повторной exact загрузкой;
- точная семантика
reusedдля valid catalog hit и repaired orphan; - передача runtime capability в shared config snapshot и fail-closed guard static card;
- revision-scoped positive/negative resolve cache в full и static card;
- актуальная проверка locale readiness при каждом danger request и отмена
открытого request при переходе в неотрисовываемый
warm; - недостающие unit/backend/smoke cases и постоянные mutation witnesses;
- bounded ожидание German route, per-file timeout и самостоятельный job timeout smoke-шарда в Validate;
- best-effort discard каждого корректного support token, ответ которого не был принят в состояние диалога;
- техническая документация и оба changelog.
Не-скоуп
- palette/файловый менеджер для отображения orphan blobs, автоматическая сборка мусора, age-based cleanup либо удаление по отсутствию ссылки;
- восстановление metadata без повторной загрузки exact canonical bytes;
- новый формат sidecar, asset id, URL, capability version, quota либо config;
- повышение
decor_assets_api, support protocol или export format; - изменение разрешений list/resolve/upload/delete и content GET;
- изменение визуала decor image, static card, danger dialog или support form;
- новое пользовательское сообщение о repaired orphan либо о техническом discard;
- изменение двухподтверждённого Area-cleanup контракта #419;
- полный рефакторинг LanguageRuntime, support pipeline или smoke sharding.
Контракт поведения
1. Physical inventory и quota
Promoted blob — обычный файл в decor-assets root с точным именем
<64 lowercase hex><allowed extension>, где extension входит в действующий
allowlist PNG/JPEG/WebP/SVG. Temporary .asset-*, sidecars, каталоги, prefix-
совпадения и неизвестные расширения не являются promoted blobs.
Quota до создания нового blob считается по физическому inventory, а не по доверенным sidecar rows:
- file count — число promoted blob-файлов;
- bytes — сумма их фактических
stat().st_size; - blob учитывается даже при отсутствующем, битом, несовпавшем либо лживом sidecar и даже при неверном digest содержимого;
- отсутствующий blob при сохранившемся sidecar не занимает file/byte quota;
- каждый реально лежащий allowed-extension файл учитывается один раз; возможные несколько расширений одного hash — несколько физических файлов.
Catalog/list/resolve при этом не становятся менее строгими: без корректного sidecar и существующего совпадающего blob orphan остаётся невидимым и не рендерится. Это разделяет доверенный каталог и физическое потребление диска.
2. Upload recovery и reused
Под upload_lock backend до отказа по count/byte quota проверяет точный путь
<validated asset_id><validated extension>:
- valid catalog row + подтвердившийся exact blob сохраняет прежний idempotent
результат
reused: trueи не меняет имя/created_at; - exact blob без принимаемого catalog row хешируется и сравнивается с id;
совпадение атомарно создаёт новый sidecar из текущей validated загрузки и
возвращает
reused: false, потому что promoted catalog entry создана этой операцией; - recovery не требует свободного file slot/bytes: физический blob уже вошёл в inventory и новых promoted bytes операция не добавляет;
- несовпавший digest не перезаписывается и не удаляется по предположению; upload fail-closed возвращает действующую безопасную ошибку целостности;
- broken/mismatched sidecar заменяется только при exact hash match того же id; ни один другой blob/sidecar не затрагивается.
Обычная новая загрузка после recovery check применяет прежние 200 assets / 256 МиБ / low-disk границы и атомарную запись. Ошибка не оставляет новый blob, sidecar или temporary.
3. Явное удаление orphan
houseplan/assets/delete по-прежнему требует write permission, корректный exact
asset id и нулевой authoritative refcount под write_lock + upload_lock.
Успешно проверенный явный запрос удаляет:
<asset_id>.json, если он существует независимо от валидности;- каждый обычный файл
<asset_id><allowed extension>из фиксированного allowlist.
Он не использует пользовательскую строку как glob, не удаляет directories,
temporary, неизвестные расширения или prefix-совпадения. removed: true, если
удалён хотя бы один exact sidecar/blob; false, если ни одного target не было.
Ошибка in_use ничего не удаляет, даже если sidecar повреждён. Это не нарушает
standing rule: причиной удаления является явное действие с точным asset id, а
не вывод из отсутствия ссылок.
4. Capability static card и resolve cache
HpConfigSnapshot получает runtime-only нормализованное поле capability. Только
exact safe integer decor_assets_api === 1 означает поддержку; отсутствующее,
нецелое, иное либо malformed значение становится null. Каждый успешный fresh
config/get авторитетен и может отозвать ранее принятую capability даже при
неизменном config fingerprint. LocalStorage snapshot capability не доверяет и
не превращает в persisted permission: до свежего ответа значение null.
houseplan-space-card:
- при exact capability вызывает resolve и рисует разрешённые assets;
- без неё не вызывает
houseplan/assets/resolve, очищает runtime map assets и использует прежнее fail-closed missing-поведение; - принимает capability-only изменение snapshot даже при неизменных config, layout и virtual-light fingerprints;
- не показывает toast/raw WS error и не добавляет новый UI.
resolveDecorAssets() получает обязательный opaque config epoch от вызывающей
карточки. Для серверного snapshot epoch включает как минимум принятую числовую
ревизию; full и static card передают ревизию одного и того же accepted config.
Cache key равен owner connection + epoch + sorted unique valid ids:
- одинаковые owner/epoch/id-set возвращают тот же
Mapбез нового WS; - новый epoch вызывает новый resolve даже при том же id-set, включая повторную
проверку прежних
missing; - разные owner не делят cache;
- failed call не кэшируется;
- batching 200 и validation ответа не меняются.
5. Danger confirmation и текущий язык
Решение о возможности открыть hp-confirm принимается по текущей конфигурации
языка и текущему состоянию LANGUAGE_RUNTIME в момент request/update, а не по
полю, записанному предыдущим render.
- Если текущая ветка
warm → noChangeне может добавить confirmation, новый_confirmDanger()сразу возвращаетfalse, ничего не регистрирует в controller и не ждёт следующего render. - Если язык уже перешёл из pending в ready/fallback до render, новый request не
получает ложный отказ из-за прошлого
warmи проходит обычный путь. - Уже открытый request при переходе в
warmразрешаетсяfalseдо потери decision source.hp-confirmперестаёт быть действующим/доступным элементом, тогда как последний стабильный body сохраняется по семантикеnoChange, а host остаётсяinertдо готовности локали. - Onboarding, fixed-floor pending/invalid, lost-space guard, cold first-load и ready main branch сохраняют контракт #402/#417; количество подтверждений — не более одного, согласие никогда не переносится между языковыми состояниями.
6. Area snapshot negative witness
Поведение #419 не меняется. Previous cleanup candidate переносится в следующий
проход только если его binding одновременно валиден, revision конечна и binding
присутствует в текущем marker_area_snapshot. Тест создаёт случай, где первые
два условия истинны, registries допустимы, но snapshot binding отсутствует;
кандидат обязан исчезнуть. Отдельная мутация снятия membership-check обязана
покраснеть именно на этом assertion.
7. Bounded smoke execution
- И
germanStarted, иgermanCompletedвdemo/smoke_danger_confirm_branches.mjsожидаются через локальную границу 1000 мс с разными диагностическими ошибками. Отсутствие completion не может превратить целевой smoke в бесконечное ожидание. - Validate smoke shard запускает каждый
demo/smoke_*.mjsчерез GNUtimeout --kill-after=10s 180s. Exit 124 считается обычным падением файла, его лог печатается и остальные назначенные shard-файлы продолжают выполняться; итог shard остаётся красным. - Job
smokeполучает собственныйtimeout-minutes: 20: это отдельная граница, которой сейчас нет, а значение соседнейperformance_smokeне считается её защитой. Детерминированное разбиение, browser install, exception guard и log artifacts не меняются.
8. Support preview token cleanup
Любой ответ houseplan/support/preview, содержащий token формы ровно 48
lowercase hex, владеет временным backend slot. Если frontend не принимает этот
ответ как текущий valid preview по любой причине, token best-effort отзывается
ровно один раз через houseplan/support/preview/discard:
- malformed hash/format/version/size/spaces/expires/text при валидном token;
- ответ устаревшего generation/draft либо снятого consent;
- отказ применить уже проверенный preview в текущее состояние.
Malformed/отсутствующий token не отправляется в discard. Cleanup выполняется
независимо от того, актуален ли уже UI request; только изменение видимого error
state зависит от current generation. Ошибка discard не маскирует исходный
support_rejected, не раскрывается пользователю и оставляет backend TTL
последней защитой. Valid current preview, успешный submit и уже существующие
close/uncheck paths не получают лишнего discard.
Модель данных, API и совместимость
- Persisted config/layout, model version, sidecar schema, asset id, export/import и support package не меняются; миграции нет.
decor_assets_api: 1уже существует вconfig/get; меняется только перенос и потребление существующей runtime capability в shared snapshot.- Новых WebSocket/HTTP endpoint и полей response нет. Уточняется смысл
существующего upload
reused:trueозначает, что valid catalog entry уже существовала до запроса; repaired orphan возвращаетfalse. - Старый frontend игнорирует additive backend поведение. Новый frontend со старым backend fail-closed не вызывает неизвестный asset resolve из static card и продолжает рисовать остальные слои.
- Orphan inventory вычисляется с диска и нигде не сохраняется. Repair sidecar использует действующую schema; downgrade не требует rollback данных.
UX, accessibility, touch, kiosk и i18n
- Новых кнопок, сообщений, focus targets, жестов, ARIA и переводов нет.
- Full View, static View, kiosk, desktop и touch получают один capability/cache контракт; редактор остаётся desktop-first.
- При несовместимом backend static card просто не рисует недоступное custom
image, как для
missing; остальные стены, комнаты, устройства и decor остаются видимы. - Danger confirmation сохраняет прежний текст, focus и alertdialog semantics; изменение только исключает зависший/устаревший dialog во время смены языка.
- Support form сохраняет текущую ошибку
support_rejected; cleanup не добавляет новый status либо toast. - Golden не требуется: ожидаемый визуальный кадр в устойчивых состояниях не
меняется. Docs screenshots должны подтвердить нулевую pixel-дельту; при
изменении
src/**обновляется их source fingerprint по штатному workflow.
Производительность, хранение и безопасность
- Quota inventory — один bounded scan директории под уже существующим
upload_lockна upload;assets/list/resolveне получают дополнительный полный scan. Стоимость O(число entries в asset root), физически ограниченном quota 200 promoted blobs плюс служебные файлы. - Повторный resolve в рамках одного config epoch остаётся O(1) cache hit; новый epoch делает не более одного batched WS-прохода на exact id-set.
- Capability guard убирает заведомо отклоняемый WS-вызов на старом backend.
- Per-file timeout ограничивает один smoke 180 секундами; самостоятельный
timeout-minutes: 20ограничивает весь shard при системном зависании либо серии отдельных отказов. Это не меняет timeout соседнейperformance_smoke. - Quota не доверяет sidecar bytes/count, delete не использует glob и всегда повторно проверяет refs/permission. Digest-mismatch никогда не перезаписывается автоматически.
- Support discard принимает только уже выданный token строгой формы и не добавляет данные к package/relay. Ошибка cleanup не раскрывает token, path или backend exception.
Затронутые модули
Ожидаемый набор; выделение чистых helpers допустимо без изменения контракта:
custom_components/houseplan/decor_assets.py— physical inventory/catalog seams;custom_components/houseplan/http_api.py— quota и orphan upload recovery;custom_components/houseplan/websocket_api.py— exact orphan delete;src/config-store.ts,src/space-card.ts,src/houseplan-card.ts,src/decor-assets.ts— capability, epoch cache и danger lifecycle;src/houseplan-editor-runtime.ts— support token cleanup;src/device-area-relocation.tsменяется только если потребуется тестируемый seam; сам контракт cleanup не меняется;test/decor-assets.test.mjs,test/device-area-relocation.test.mjs, новый либо существующий config/static-card contract test;tests_backend/test_decor_assets.py,tests_backend/test_ha_websocket.py— inventory/upload/delete cases;demo/smoke_danger_confirm_branches.mjs,demo/smoke_support_feedback.mjs— transition/cleanup integration;.github/workflows/validate.yml, workflow contract test,scripts/mutation-gate.mjs— timeouts и постоянные свидетели;docs/ARCHITECTURE.md,docs/CONFIG-COMPATIBILITY.md,docs/TESTING.md,docs/SUPPORT-PRIVACY.md, оба changelog и docs screenshot fingerprint.
Критерии приёмки
- AC1 (backend/unit, storage). Blob без sidecar и blob с malformed/ mismatched sidecar отсутствуют в catalog/list/resolve, но каждый входит в physical count и actual-byte quota; sidecar без blob не входит в catalog и не расходует blob quota. Доказательство: pure backend matrix с границами count/bytes ±1.
- AC2 (backend/HA, recovery). Exact повторная загрузка valid orphan до
проверки новой quota атомарно создаёт sidecar, возвращает
reused:falseи делает asset доступным list/resolve; последующая identical upload возвращаетreused:true. Digest mismatch не меняет ни один файл. Доказательство: upload endpoint test с full-quota fixture и hash asserts. - AC3 (backend/HA, explicit delete). Writer удаляет unreferenced exact
blobs всех allowed extensions и sidecar даже при missing/broken metadata;
removedотражает наличие удалённого target.in_use, prefix, unknown ext, temp и directory остаются нетронутыми. Доказательство: WebSocket access / filesystem matrix. - AC4 (backend/unit, regression). Valid-shaped sidecar без matching blob
отбрасывается общим catalog/direct-read validator; снятие
blob.is_file()краснит точный тест. Доказательство: расширенныйtest_catalog_ignores_missing_or_malformed_sidecars+ mutation gate. - AC5 (unit/smoke, compatibility). Fresh shared snapshot нормализует exact
capability, localStorage seed имеет
null; static card не вызывает resolve без exact v1, очищает asset map при downgrade и вызывает resolve после capability-only upgrade при том же config. Доказательство: shared-store unit + static-card network-counter smoke/contract. - AC6 (unit, performance). Одинаковые owner/epoch/sorted-id-set дают один
resolve и один
Map; при следующем config epoch тот же положительный либо negative-cached set вызывает новый resolve. Failed call не кэшируется, batching остаётся 200. Доказательство:decor-assetscall-count matrix. - AC7 (browser smoke, safety). В обоих окнах ready→warm и warm→ready новый
danger request принимает решение по текущему runtime; уже открытый dialog при
переходе в warm резолвится
false, исчезает как действующий decision source, controller пуст, stable body не заменён, host inert. Остальные ветви #417 остаются зелёными. Доказательство: расширенный branch smoke с bounded promise races и двумя отдельными mutants. - AC8 (unit, regression). Previous Area cleanup candidate, отсутствующий в
текущем snapshot, не переносится при otherwise-valid данных; снятие
snapshotBindings.has(binding)краснит только этот кейс. Доказательство: targeted unit + mutation gate. - AC9 (unit/CI contract, liveness).
germanCompletedпадает с собственной диагностикой не позднее 1000 мс; каждый CI smoke имеет 180-секундный TERM и 10-секундный KILL guard, timeout помечает shard красным, сохраняет лог и не пропускает последующие файлы; сама jobsmokeимеет независимыйtimeout-minutes: 20. Доказательство: completion-timeout probe + workflow unit с поочерёдно удалёнными per-file wrapper и job-level timeout. - AC10 (browser smoke, privacy/lifecycle). Valid token из invalid, stale или неприменённого preview response отзывается ровно один раз; malformed token не отзывается; current valid preview и success не получают преждевременный discard. UI сохраняет исходный error/current draft. Доказательство: support smoke с WS call counters + mutation gate.
- AC11 (review/docs, compatibility). Нет schema/API version/i18n/visual изменений; standing no-inference deletion, write/ref guards и partial asset resolve сохранены. Architecture/compatibility/testing/privacy docs и оба changelog согласованы. Доказательство: diff review, docs checks и compatibility fixtures.
- AC12 (gates). Typecheck, unit, build/bundle sync, selected frontend smokes, backend tests, no-new-any, docs, workflow contracts и все новые mutation witnesses зелёные на exact SHA. Golden и full performance остаются предрелизными, Linux CI — канон полного HA harness.
Таблица защитных доказательств
Точные имена тестов можно уточнить при реализации, но каждая строка сохраняет отдельный отрицательный witness по правилу #435.
| AC | Чем доказан | Чем обязан покраснеть |
|---|---|---|
| AC1 | backend quota/catalog matrix | inventory снова строится из read_catalog() либо доверяет sidecar bytes; orphan не меняет count/bytes и boundary upload ложно проходит |
| AC2 | HA upload recovery test | recovery выполняется после quota либо возвращает reused:true; full-quota repair отказывается или assertion response падает |
| AC3 | HA delete filesystem matrix | delete снова зависит только от valid catalog row либо удаляет glob/prefix; orphan остаётся или sentinel исчезает |
| AC4 | exact sidecar-without-blob test | mutation удаляет blob.is_file(); missing blob появляется в catalog |
| AC5 | config-store/static-card call counters | mutation удаляет exact capability guard/revocation; старый backend получает resolve либо downgrade сохраняет map |
| AC6 | resolve cache call-count unit | mutation удаляет epoch из key; второй epoch не делает WS и прежний missing остаётся |
| AC7 | danger branch smoke | mutations возвращают cached render gate либо удаляют warm-transition cancel; promise зависает/ложно отклоняется или controller/DOM остаётся действующим |
| AC8 | Area cleanup targeted unit | mutation удаляет snapshotBindings.has(binding); отсутствующий binding переносится |
| AC9 | timeout probe + workflow contract | mutation делает plain await germanCompleted, plain node "$f" либо удаляет smoke.timeout-minutes; probe превышает границу или contract assertion не находит одну из двух независимых границ |
| AC10 | support smoke с exact discard counters | mutation переставляет validation throw до cleanup/удаляет cleanup; token не отзывается либо отзывается дважды |
Для AC1–AC4/AC7/AC8/AC10, где полный backend/browser прогон дорог для ручного
повтора, добавляются persistent entries в scripts/mutation-gate.mjs. Для AC5,
AC6 и workflow contract допустим адресный unit red-run, если его команда и вывод
внесены в code-review; пустого третьего столбца быть не может.
План автотестов
- Создать backend filesystem matrix: valid row, missing blob, missing sidecar, malformed/mismatched sidecar, wrong digest, duplicate allowed extensions, unknown extension, temp и directory; независимо проверить trusted catalog и physical inventory.
- На count и byte limit проверить обычный отказ, exact orphan repair при уже
полном store и честную последовательность
false → trueдляreused. - Через HA WebSocket проверить delete для orphan/broken/valid metadata, writer/ read-only, in-use race и sentinels, которые не совпадают exact id+allowlist.
- Расширить
decor-assetsunit матрицей owner × epoch × id order × missing × rejected call × 200/201 ids. - Проверить shared snapshot для fresh exact/missing/malformed capability, localStorage seed и capability-only upgrade/downgrade без смены config body.
- В danger branch smoke удерживать locale request: отдельно открыть request до warm transition, вызвать request до следующего render после смены языка и завершить locale до следующего render. Все ожидания имеют локальный timeout.
- Добавить exact Area candidate кейс и выполнить его с удалённым membership condition.
- Добавить German completion timeout probe и статический workflow contract;
timeout fixture обязан оставить следующий synthetic command выполненным, но
итоговый status — failure; отдельная проверка отличает 20-минутный timeout
smokeот одноимённого поля соседнейperformance_smoke. - В support smoke вернуть ответы с valid token и отдельно испорченными hash, format, size и current generation; посчитать discard по каждому token.
- Запустить штатное дерево и каждый mutation witness: падение должно происходить по целевому assertion, а не по parse/import/внешнему глобальному timeout.
Риски
- Физический scan примет посторонний файл за asset. Смягчение: exact hash filename + фиксированный allowlist; это консервативно для quota, но никогда не делает файл доверенным catalog asset.
- Recovery перезапишет чужие bytes. Смягчение: exact path и полный SHA-256; mismatch только отказывает и требует явного delete.
- Delete расширит область удаления. Смягчение: fixed paths из allowlist, no glob, permission/ref locks и sentinel tests.
- Capability-only snapshot потеряется из-за fingerprint optimization. Смягчение: capability включена в adoption decision и имеет отдельный тест без config/layout delta.
- Danger fix нарушит
noChangeи мигнёт stable body. Смягчение: smoke сравнивает body identity/содержимое отдельно от удаляемого confirm outlet. - Per-file timeout окажется слишком коротким на cold runner. Смягчение: 180 секунд значительно выше обычного отдельного smoke, job получает отдельные 20 минут на установку и весь shard; timeout печатает точное имя и лог для пересмотра числа.
- Best-effort discard сам упадёт. Смягчение: исходная ошибка остаётся основной, TTL backend сохраняется как финальная защита.
Откат
Откат — единый revert implementation commit. Persisted schema и migration отсутствуют. Уже восстановленные valid sidecars остаются обычными корректными catalog rows и безопасны для старой версии. Возврат старой quota/cache/confirm/ cleanup логики допустим только вместе с возвратом соответствующих тестов и changelog; пользовательские blobs при откате автоматически не удаляются.
Release-артефакты
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.md: orphan recovery/quota/delete, static capability/cache retry и безопасные async cleanup fixes;docs/ARCHITECTURE.md: trusted catalog против physical inventory и asset recovery/delete boundaries;docs/CONFIG-COMPATIBILITY.md: runtime-only capability static card и cache epoch без повышения API;docs/TESTING.md: 180-секундный per-smoke guard и negative witness policy;docs/SUPPORT-PRIVACY.md: token из непринятого ответа также отзывается;scripts/mutation-gate.mjs: постоянные backend/browser witnesses;- docs screenshot workflow: подтверждение нулевой pixel-дельты и обновлённый
fingerprint из-за
src/**; - i18n, user guide, golden baselines и performance profiles: без изменений;
- implementation commit —
User-Visible: yes, оба changelog в том же коммите.
Принятые технические предположения
Эти решения не меняют продуктовый замысел и могут быть свободно скорректированы ревьюером до S5:
- orphan остаётся невидимым в palette до exact повторной загрузки: без sidecar нельзя достоверно показать имя, MIME и размеры;
- quota считает фактические allowed-extension files, а не уникальные hash: каждое физическое потребление диска должно быть ограничено;
- broken sidecar repair получает metadata/имя/created_at текущей загрузки и
reused:false; прежним недоверенным полям sidecar не следуем; - explicit delete по hash охватывает все allowed extensions этого exact hash, потому что пользователь назвал content identity, а не одно недостоверное расширение из sidecar;
- config cache epoch передаётся явно в
resolveDecorAssets, а capability в localStorage не сохраняется; свежий server response — единственный authority; - 1000 мс для route-completion, 180 секунд + 10 секунд kill grace для файла и
20 минут для job
smoke— технические liveness budgets, не пользовательские таймауты; - ссылки на строки из аудита ориентировочны: реализация привязывается к символам
и проверяемому поведению на актуальном
dev.