# SPEC-REVIEW-554-r1 ## Скоуп Issue #554 (лёгкий трек, `small`, `bug`, `P3`): при attachment upload low-disk guard (`check_quota` в `custom_components/houseplan/plans.py`) повторно вычитает из `disk_usage().free` размер staging-файла, который `/api/houseplan/upload` уже полностью записал на диск до вызова guard'а. Promotion — `os.replace()` в пределах одного `files_root`, второй копии не требуется, поэтому около порога `MIN_FREE_BYTES = 512 MiB` валидная загрузка получает ложный `low_disk_space`. ТЗ живёт в теле issue #554 под `## ТЗ` (owner-решение #517, дата ТЗ 2026-09-12, после отсечки 2026-09-10 — файл в `docs/specs/` не заводится, и это верно). Трек `small` подтверждён аналитикой владельца в комментарии `S2` от 2026-09-12: сложность 3/10, одна поверхность, без миграции, нового UX-контракта, влияния на perf/touch — критерии §5 совпадают, отказ от лёгкого трека не обоснован и не требуется. Материал ревью — тело issue #554 на момент чтения (аналитика владельца уже оставлена как последний комментарий; ТЗ ниже неё не менялось после этого). Заход r1, первый цикл ревью ТЗ для этой задачи. ## Как проверялось Ревью ТЗ на этапе `spec` — сверка утверждений ТЗ с фактическим кодом на `origin/dev` (`3aa9ffbd`), а не доверие рассказу автора: - прочитан `docs/SCOPE.md` — задача попадает в J4/J6 (сохранность и пригодность пользовательских вложений), конфликта с mission/out-of-scope нет; - прочитан `AGENTS.md`, `PROCESS.md` §2.4, §5, §7.1 — требования к разделам ТЗ на лёгком треке (проблема · контракт · AC1…ACn с доказательством · откат) и порядок ревью; - прочитан `docs/USER-GUIDE.ru.md` (раздел лимитов, строка про 512 МБ) — формулировка порога для пользователя не меняется этим ТЗ, значит правка документации на уровне UI-текста не требуется; - прочитан `custom_components/houseplan/plans.py` (`check_quota`, `dir_usage`, `MIN_FREE_BYTES` в `const.py`) и `custom_components/houseplan/http_api.py` (`HouseplanUploadView.post`, строки ~440–460) — подтверждено буквально: staged-файл пишется на диск ДО вызова `check_quota(..., incoming=tmp_path.stat().st_size, exclude=tmp_path)`, и guard вычитает `incoming` второй раз из уже уменьшенного `disk_usage().free`; - проверено единственное другое место вызова `check_quota` — `websocket_api.py:2349` (plan upload, `check_quota(plans_dir, len(raw), ...)` **до** `path.write_bytes(raw)`) — это действительно «ещё не записанный» путь, и утверждение ТЗ о нём («сохраняет прежний reserve по умолчанию») корректно; - отдельно проверен decor-asset upload (`HouseplanDecorAssetView._store`, http_api.py:225–338): он не использует `check_quota` вовсе и держит файл в памяти (`blocks: list[bytes]`) до собственного инлайн-чека `shutil.disk_usage(root).free - len(validated.data) < MIN_FREE_BYTES` **до** записи на диск — тоже «ещё не записанный» путь, и то, что ТЗ не включает этот файл/путь в затронутые модули, соответствует факту, а не пропуск; - прочитаны существующие тесты `tests_backend/test_validation.py` (`test_issue_498_check_quota_excludes_the_staged_upload_itself`, `test_check_quota_refuses_when_the_disk_is_nearly_full`) и `tests_backend/test_ha_upload.py` (конкурентный upload-тест с `threading.Barrier`, строки ~122–140) — подтверждают, что #498 (double-count в store byte/count quota) уже закрыт отдельно от диск-guard'а, и что инфраструктура для boundary/concurrency тестов, на которую опирается план автотестов ТЗ, уже существует и расширяема без новой механики; - проверено, что `scripts/mutation-gate.mjs` уже содержит мутанты с `guard: 'python3 -m pytest tests_backend/...'` для других backend-контрактов — заявленный в AC5 механизм (именованный мутант на backend-защиту) не является новой инфраструктурой, а используется тем же способом, что и в проекте ранее; - проверено `docs/CONFIG-COMPATIBILITY.md` на предмет quota/disk-полей — единственное упоминание quota касается несвязанного decor-asset дедупликации по хешу; утверждение ТЗ «миграция и compatibility-поля не нужны» ничем не опровергается. Гейты (typecheck/test/build) на этом этапе не прогонялись: этап `spec` проверяет постановку, а не код — кода по этой задаче ещё нет (только `.dev` на `3aa9ffbd`, без изменений в `custom_components/**`). Это осознанный пропуск, а не необходимость: ревью ТЗ гейтов не гоняет по `PROCESS.md` §2.4/§8 — они появляются в код-ревью. ## Находки Ни одной. Ни High, ни Medium, ни Low. ## Что проверено и корректно - **Соответствие `docs/SCOPE.md`.** Задача закрывает J4/J6 (сохранность и пригодность прикреплённых пользовательских файлов); правки продукта нет — это устранение ложного отказа существующей функции, а не новая возможность. Конфликтов с out-of-scope и с правилом «никогда не удалять файл пользователя по догадке» нет — cleanup/promotion не меняются (п.5 контракта, AC4). - **Соответствие §5 (лёгкий трек).** Все обязательные разделы присутствуют: проблема (§1), контракт поведения (§2), критерии приёмки с доказательством (§3), откат (§6). Дополнительно (не обязательно, но полезно и корректно) — затронутые файлы (§4), совместимость (§5) и явный блок принятых предположений (§7). - **Однозначность контракта.** Пять пунктов §2 ТЗ описывают ровно одно изменение: новый параметр `additional_disk_bytes` у `check_quota`, по умолчанию равный `incoming` (обратная совместимость для plan upload и любых будущих вызовов «до записи»), attachment-путь передаёт `0` и `exclude=tmp_path`. Разделение «логический размер для store-квоты» и «сколько ещё физически предстоит записать» соответствует буквально тому, что делает код сегодня одной функцией на два смысла сразу — источник дефекта назван точно. - **Каждый AC проверяем и имеет способ доказательства.** AC1 и AC2 задают точные граничные значения (`free == MIN_FREE_BYTES` / `MIN_FREE_BYTES - 1` для staged; `MIN_FREE_BYTES + incoming` / `... - 1` для not-yet-written) — оба воспроизводимы через существующий паттерн `monkeypatch.setattr(shutil, "disk_usage", ...)`, уже использованный в `test_check_quota_refuses_when_the_disk_is_nearly_full`. AC3 и AC4 — регрессионные, привязаны к именованным существующим тестам #498. AC5 — защитный AC с явно названным механизмом доказательства (мутант в `scripts/mutation-gate.mjs`, тот же механизм что и у других backend-защит в проекте) и корректно требует, чтобы ревьюер кода увидел «тест умеет падать», а не поверил слову. - **Никаких выданных за факт догадок.** Все допущения (§7 ТЗ) явно помечены как предположения и подтверждаются кодом независимо от авторского заявления: staging и итоговый файл — один `files_root`/один том (иначе `os.replace()` в `_promote()` не работал бы через директории уже сейчас); `disk_usage().free` меряется после записи staging и отражает чужие staged-файлы тоже — это свойство самого syscall, а не решение автора; новый параметр — внутренний контракт, не раскрывается пользователю (проверено: `docs/USER-GUIDE.ru.md` не описывает внутренние параметры quota-функций, только конечный порог 512 МБ, который не меняется). - **Продуктовых вопросов владельцу нет и не требовалось.** Видимое поведение однозначно: сейчас — ложный отказ загрузки вложения около порога; после — успешная загрузка при фактически достаточном месте. Погранично-продуктовых развилок (какая персона важнее, что считать приемлемой деградацией) в задаче нет — это чистое исправление дефекта в рамках уже описанного контракта (512 МБ резерва), а не новый UX. - **Один источник числа.** Единственная пользователь-видимая величина — порог 512 МБ (`MIN_FREE_BYTES`) — не дублируется и не меняется; ТЗ явно фиксирует, что глобальный резерв остаётся прежним (§5 «Совместимость и ограничения»). Правило «одно число — один источник» не задето. ## Чего не проверял - Собственно код исправления — его ещё нет (этап `spec`, статус `S4-spec-review`, класс A не тронут). Все проверки кода выше — это сверка утверждений ТЗ с **текущим** (нечинённым) поведением, а не аудит будущей реализации. - Гейты `typecheck`/`test`/`build`/`pytest tests_backend` — не прогонялись, не относится к этапу spec; будут частью код-ревью. - Полнота списка call sites `check_quota` подтверждена `grep` по всему `custom_components/houseplan/**`, но не покрывает гипотетические внешние вызовы (плагины/интеграции) — House Plan не предоставляет публичного Python API для этого модуля, риск считаю нулевым. ## Вердикт Зелёный. ТЗ полно для лёгкого трека, каждый AC однозначен и доказуем, технические утверждения проверены по коду и подтвердились буквально, продуктовых вопросов не осталось. --- ## Материал раунда - Issue: #554 - Дерево ТЗ: тело issue #554, раздел `## ТЗ`, состояние на момент чтения (после последнего комментария владельца `S2` от 2026-09-12T20:38:57Z, без последующих правок тела). - Referenced code (для сверки утверждений ТЗ, не как объект правки): `origin/dev` `3aa9ffbd1a1ac1970a91cedbc3d0d6f0a521692a`. - Заход: r1. Блокирующих циклов израсходовано: 0/2 (лимит лёгкого трека). --- ## Материал раунда - Ветка: `dev`, коммит `3aa9ffbd1a1a` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `c6b6b6f11f936b2a4d55991381f14406f1b377b2` ``` git log --all --format='%H %T' | grep c6b6b6f11f93 ``` - Тело issue: `3108990578ecf761611c8830494881c27f6d0cffdef2f0d283304bd9c32968e7` - Вердикт конвейера: `green` · High 0