14 KiB
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/dev3aa9ffbd1a1ac1970a91cedbc3d0d6f0a521692a. - Заход: r1. Блокирующих циклов израсходовано: 0/2 (лимит лёгкого трека).
Материал раунда
- Ветка:
dev, коммит3aa9ffbd1a1a— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
c6b6b6f11f936b2a4d55991381f14406f1b377b2git log --all --format='%H %T' | grep c6b6b6f11f93 - Тело issue:
3108990578ecf761611c8830494881c27f6d0cffdef2f0d283304bd9c32968e7 - Вердикт конвейера:
green· High 0