docs: review document for #554

Issue: #554
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-12 20:44:46 +00:00
parent 3aa9ffbd1a
commit 49130d4ffd
+180
View File
@@ -0,0 +1,180 @@
# 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 (лимит лёгкого трека).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `3aa9ffbd1a1a` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `c6b6b6f11f936b2a4d55991381f14406f1b377b2`
```
git log --all --format='%H %T' | grep c6b6b6f11f93
```
- Тело issue: `3108990578ecf761611c8830494881c27f6d0cffdef2f0d283304bd9c32968e7`
- Вердикт конвейера: `green` · High 0