docs: specify v1.71 audit polish

Issue: #434
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-03 10:55:15 +00:00
committed by claude[bot]
parent d1091b6d47
commit 8a75c53eca
2 changed files with 512 additions and 0 deletions
+511
View File
@@ -0,0 +1,511 @@
# ТЗ #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 и закрепить для
каждой защитной ветки отрицательный свидетель:
1. физический учёт и явное удаление orphan decor blobs;
2. capability guard в `houseplan-space-card`;
3. cache epoch resolve по ревизии config;
4. точный тест наличия blob для catalog row;
5. честный `reused` при восстановлении sidecar;
6. актуальный locale gate и симметричная отмена danger confirmation;
7. свидетель принадлежности кандидата текущему Area snapshot;
8. локальные timeout для German wait и каждого CI smoke-файла;
9. отзыв валидного support token из непринятого ответа.
## Подтверждённые причины
1. `read_catalog()` обходит только `*.json`, а `_read_catalog_row()` принимает
строку лишь при существующем blob. Поэтому blob без корректного sidecar не
виден ни каталогу, ни quota, а delete сначала ищет ту же невидимую строку.
2. `HpConfigSnapshot` не переносит `decor_assets_api`, и `_load()` static card
безусловно вызывает `houseplan/assets/resolve`.
3. `resolveCache` хранит одну пару `набор id → Map` на connection и не включает
принятую серверную ревизию config; `missing` тем самым становится бессрочным
для прежнего набора id.
4. Тест с названием `test_catalog_ignores_missing_or_malformed_sidecars`
создаёт blob для единственной валидной строки и не проверяет обратный случай
«валидный sidecar, blob отсутствует».
5. Upload при уже существующем exact blob дописывает sidecar и возвращает
`reused: true`, хотя до операции promoted catalog entry не существовала.
6. `_confirmDanger()` читает `_dangerConfirmLocaleGate` прошлого render. Между
изменением языка и render это даёт ложное разрешение, а между завершением
загрузки языка и render — ложный отказ. Переход в `warm/noChange` не отменяет
уже открытый controller request и оставляет его в inert DOM.
7. Условие `snapshotBindings.has(binding)` в `resolveAreaSnapshotCleanup()` не
имеет кейса, в котором все соседние проверки истинны, а binding отсутствует
только в текущем snapshot.
8. `germanStarted` ограничен одной секундой, соседний `germanCompleted` ожидается
без границы; CI запускает каждый `demo/smoke_*.mjs` простым `node`, поэтому
отдельный файл ограничивает лишь 20-минутный timeout всей job.
9. `_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 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` через GNU
`timeout --kill-after=10s 180s`. Exit 124 считается обычным падением файла,
его лог печатается и остальные назначенные shard-файлы продолжают выполняться;
итог shard остаётся красным.
- Глобальный `timeout-minutes: 20`, детерминированное разбиение, 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 секундами и не уменьшает
20-минутный общий бюджет job.
- 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-assets` call-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 красным, сохраняет лог и не
пропускает последующие файлы. **Доказательство:** completion-timeout probe +
workflow unit с мутированным отсутствующим wrapper.
- **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"`; probe превышает границу или contract assertion не находит timeout wrapper |
| 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; пустого третьего столбца быть не может.
## План автотестов
1. Создать 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.
2. На count и byte limit проверить обычный отказ, exact orphan repair при уже
полном store и честную последовательность `false → true` для `reused`.
3. Через HA WebSocket проверить delete для orphan/broken/valid metadata, writer/
read-only, in-use race и sentinels, которые не совпадают exact id+allowlist.
4. Расширить `decor-assets` unit матрицей owner × epoch × id order × missing ×
rejected call × 200/201 ids.
5. Проверить shared snapshot для fresh exact/missing/malformed capability,
localStorage seed и capability-only upgrade/downgrade без смены config body.
6. В danger branch smoke удерживать locale request: отдельно открыть request до
warm transition, вызвать request до следующего render после смены языка и
завершить locale до следующего render. Все ожидания имеют локальный timeout.
7. Добавить exact Area candidate кейс и выполнить его с удалённым membership
condition.
8. Добавить German completion timeout probe и статический workflow contract;
timeout fixture обязан оставить следующий synthetic command выполненным, но
итоговый status — failure.
9. В support smoke вернуть ответы с valid token и отдельно испорченными hash,
format, size и current generation; посчитать discard по каждому token.
10. Запустить штатное дерево и каждый 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 и сохраняет глобальные
20 минут; 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 для файла —
технические liveness budgets, не пользовательские таймауты;
- ссылки на строки из аудита ориентировочны: реализация привязывается к символам
и проверяемому поведению на актуальном `dev`.
+1
View File
@@ -180,6 +180,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#340](https://github.com/Matysh/houseplan-card/issues/340) Обязательная ревизия повторной записи config/set | [340-config-set-revision.md](340-config-set-revision.md) |
| [#372](https://github.com/Matysh/houseplan-card/issues/372) Компактное верхнее кадрирование static card без заголовка | [372-space-card-empty-title.md](372-space-card-empty-title.md) |
| [#423](https://github.com/Matysh/houseplan-card/issues/423) Полиш support pipeline и защитных инструментов v1.70.0 | [423-v170-polish.md](423-v170-polish.md) |
| [#434](https://github.com/Matysh/houseplan-card/issues/434) Полиш аудита v1.71.0-beta.1 | [434-v171-polish-audit.md](434-v171-polish-audit.md) |
## P3