mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
181 lines
14 KiB
Markdown
181 lines
14 KiB
Markdown
# 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
|