diff --git a/docs/reviews/INDEX.md b/docs/reviews/INDEX.md index a73c1da4..b0a4f2ed 100644 --- a/docs/reviews/INDEX.md +++ b/docs/reviews/INDEX.md @@ -1,6 +1,6 @@ # Индекс ревью -Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 1010, issue: 352. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`. +Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 1011, issue: 353. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`. | Issue | Документ | Этап · раунд | Вердикт | H | M | Находки | Файлы | |---|---|---|---|---:|---:|---|---| @@ -26,6 +26,7 @@ | #621 | [CODE-REVIEW-621-r1.md](CODE-REVIEW-621-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — | | #619 | [CODE-REVIEW-619-r1.md](CODE-REVIEW-619-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — | | #618 | [SPEC-REVIEW-618-r1.md](SPEC-REVIEW-618-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | нотация h в B5 не встречается в коде | `docs/FILTERING.md` | +| #617 | [SPEC-REVIEW-617-r1.md](SPEC-REVIEW-617-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | «новый необязательный параметр» уже существует | `src/backdrop-pick.ts` `houseplan-editor-runtime.ts` | | #615 | [SPEC-REVIEW-615-r1.md](SPEC-REVIEW-615-r1.md) | spec · r1 | 🟡 жёлтый | 0 | 1 | AC3 называет несуществующую защиту от расползания на плашки цвета | `smoke_room_settings_form.mjs` `smoke_space_settings_form.mjs` `smoke_device_settings_form.mjs` `smoke_dialog_polish_605.mjs` `smoke_general_settings_form.mjs` | | #614 | [SPEC-REVIEW-614-r1.md](SPEC-REVIEW-614-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | — | — | | #614 | [CODE-REVIEW-614-r1.md](CODE-REVIEW-614-r1.md) | code · r1 | 🟡 жёлтый | 0 | 1 | AC1/AC2/AC3 требуют unit-тест, а он не написан | `test/dialog-baseline.test.mjs` `test/space-dialog.test.mjs` `general-form-state.ts` `space-form-state.ts` `marker-form-state.ts` `tsconfig.test.json` `dialog-baseline.ts` `scripts/mutation-registry.mjs` | diff --git a/docs/reviews/SPEC-REVIEW-617-r1.md b/docs/reviews/SPEC-REVIEW-617-r1.md new file mode 100644 index 00000000..e443c2f5 --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-617-r1.md @@ -0,0 +1,229 @@ +# SPEC-REVIEW-617-r1 + +**Issue:** [#617](https://github.com/Matysh/houseplan-card/issues/617) — «Загрузка плана > ~3 МиБ обрывает WebSocket без сообщения: MAX_PLAN_BYTES 8 МиБ недостижим через 4 МиБ кадр» +**Этап:** ТЗ на ревью (PROCESS.md §2.4), трек `small` +**Заход:** r1 · блокирующих циклов израсходовано 0 из 2 (лимит лёгкого трека) +**Ревьюер:** независимая сессия, без контекста автора ТЗ + +**Вердикт: зелёный** + +--- + +## Скоуп ревью + +ТЗ живёт в теле issue #617, раздел `## ТЗ` (решение владельца #517 — файл в +`docs/specs/` не создаётся). Материал ревью — ровно этот текст, см. блок +«Материал раунда» в конце документа. + +Задача: 4 МиБ дефолтный кадр WS aiohttp делает объявленный `MAX_PLAN_BYTES = 8 +МиБ` недостижимым — файл плана > ≈3 МиБ рвёт соединение вместо честной ошибки +`too_large`. Выбран вариант (б): новый HTTP-view `POST +/api/houseplan/plans/upload` со стриминговым пределом, карточка грузит план +только через него; WS `houseplan/plan/set` остаётся для старых закешированных +карточек без изменения контракта. + +## Как проверялось + +Ревью состязательное: identifiers и утверждения о существующем поведении из ТЗ +сверены с реальным кодом на `dev` (SHA см. ниже), а не приняты на слово автора. + +Прочитано перед вынесением вердикта: +- `docs/SCOPE.md` (персона «Home admin», out-of-scope список — задача не + задевает ни один пункт: не CAD/PDF/3D/радар/оверлей, обычная правка + editor-only поверхности); +- `PROCESS.md` §1–§10 (классы файлов, ЖЦ, лёгкий трек §5, лимит циклов §4, + формат вердикта §7.2); +- `AGENTS.md` (роли, трейлеры, гейты); +- `docs/TOUCH-SUPPORT.md` (проверка утверждения ТЗ «touch не затронут» — + подтверждено: правка живёт в editor-only диалоге пространства, View/kiosk не + трогается, а редакторы по продукту desktop-first); +- само тело issue #617 целиком, включая аналитический комментарий-обоснование + трека `small`. + +Код читан выборочно, но по каждому явному утверждению ТЗ о существующем +поведении — не по касательной: + +| Утверждение ТЗ | Файл:строка | Результат сверки | +|---|---|---| +| `MAX_PLAN_BYTES = 8 * 1024 * 1024` в `validation.py` | `custom_components/houseplan/validation.py:24` | совпадает | +| `ws_plan_set` шлёт `too_large` при `len(raw) > MAX_PLAN_BYTES`, копия-при-записи `..`, `check_quota`+`atomic_write` под `upload_lock` | `custom_components/houseplan/websocket_api.py:2330–2384` | совпадает | +| `may_write` / `_check_write` — общая точка авторизации WS и HTTP | `custom_components/houseplan/auth.py:16`, `websocket_api.py:306` | совпадает, один helper | +| `HouseplanDecorAssetUploadView` буферизует тело в памяти блоками, `_FLUSH_AT`-преflight по `Content-Length` | `custom_components/houseplan/http_api.py:218–352`, `:68` | совпадает — это и есть ближайший шаблон для «принято предположительно» п.5 | +| `HouseplanUploadView` — паттерн `fetchWithAuth` + `FormData`, разбор `err.too_large`/`err.unauthorized`/`err.bad_ext` | `houseplan-editor-runtime.ts:7371–7412` | совпадает, включая точный текст `err.too_large` с `{mb}` из ответа | +| `classifyPlanFile(file, guardAboveBytes)` — svg проходит без проверки размера, растр уходит в guard при `size > guardAboveBytes` независимо от `probe.kind` | `src/backdrop-pick.ts:87-99` | совпадает: раздельная обработка SVG/растра в ТЗ верна | +| `renderBackdropGuard(..., allowOriginal = true)` | `src/backdrop-pick.ts:157-163`, вызов для декора `houseplan-editor-runtime.ts:7949` (`allowOriginal = size <= 2 МиБ`) | **параметр уже существует и уже используется декором** — см. находку Low ниже | +| `PlanFilePayload.b64`, `space-dialog.ts` тип, `space-form-state.ts` сравнивает только `.name` | `src/backdrop-pick.ts:14-19`, `src/space-dialog.ts:17`, `src/editors/space-form-state.ts:30` | совпадает | +| `_saveSpaceDialog`: «загрузить ДО изменения конфига», перечитывание конфига в catch, `toast.error` | `houseplan-editor-runtime.ts:8084-8096`, `:8213-8227` | совпадает | +| `probeBackdrop` даёт ровно `safe/warn/hard/unknown` | `src/backdrop-probe.ts:29,50-52` | совпадает | +| USER-GUIDE уже обещает «8 МБ» (ru/en) | `docs/USER-GUIDE.ru.md:2225`, `docs/USER-GUIDE.md:1385` | совпадает | +| `PLAN_EXTENSIONS`, `valid_space_id`, `CONTENT_URL`, `atomic_write`, `check_quota` | `validation.py:22,1156`, `const.py:21`, `plans.py:29,240` | все существуют, сигнатуры как в ТЗ | +| `demo/smoke_backdrop_guard.mjs` сегодня сверяет `planFile.b64` побайтово («Оставить оригинал») | `demo/smoke_backdrop_guard.mjs:73-84,108,270` | совпадает — правка смока действительно необходима и её объём соответствует заявленному | +| `scripts/mutation-registry.mjs`, `scripts/mutation-gate.mjs`, `scripts/check-inputs.mjs`, `scripts/smoke-select.mjs` существуют | `ls scripts/*.mjs` | совпадает | + +Отдельно проверена «граница включительная» (AC: `MAX_PLAN_BYTES` проходит, +`+1` отклоняется): и `ws_plan_set` (`> MAX_PLAN_BYTES`), и +`HouseplanDecorAssetUploadView` (`> MAX_DECOR_ASSET_BYTES`) используют строгое +`>`, т.е. равенство пределу проходит — контракт для нового view, списанный с +этого же шаблона, воспроизводим и непротиворечив. + +Это не гейт-прогон (стадия ТЗ, кода ещё нет) — типизация/тесты/сборка здесь +неприменимы. Проверка кода была вместо «поверил автору» — цель прочитанного +выше. + +## Разделы §7.1 — присутствуют все + +Сценарий · что человек увидит до/после · проблема · скоуп/не-скоуп · контракт +поведения · UX · модель данных и миграция (явное «нет») · i18n · AC1…AC8 с +таблицей «чем доказан / чем краснеет» · план автотестов · перф/touch · риски · +откат · release-артефакты · «принято предположительно» (8 пунктов). Пункты DoR +§2.5 покрыты: миграция/compatibility — явное «нет» со ссылкой на +`CONFIG-COMPATIBILITY.md`; touch — явное «не затронут» с корректным +обоснованием через персону/поверхность; открытых продуктовых вопросов нет. + +## AC — проверка однозначности и доказуемости + +AC1–AC8 пронумерованы, у каждого в таблице указан и тест, и мутация/негативная +проба («чем краснеет») — защитные AC (AC2–AC5, AC7 частично) не остаются без +названного свидетеля. Разобраны предметно: + +- **AC1** — «PNG, проба `safe`» как явное условие теста снимает + недетерминированность (без этого уточнения 5 МиБ файл мог бы попасть в guard + по `warn`); формулировка «ровно один POST … и ни одного `plan/set`» считается + счётчиком, не текстом — однозначно. +- **AC2/AC3** — раздельные ветки SVG/растра в контракте совпадают с реальным + разделением в `classifyPlanFile` (SVG обходит проверку размера в самой + функции, поэтому проверка предела для SVG обязана жить отдельно в + `_pickPlanFile` — ТЗ описывает это как отдельный пункт списка, а не путает + два пути). Условие AC3 «с читаемыми размерами» корректно исключает + `hard`/`unknown`, для которых контракт сознательно не меняет поведение (см. + находку Low о непокрытом случае — не блокирует). +- **AC4** — таблица кодов ответа полная и без пересечений (403/503/413×2/400×3/507/200); + граница `MAX`/`MAX+1` и отсутствие временных файлов после 413 — измеримо + прогоном backend-теста. +- **AC5** — единственный AC, доказывающий защиту «клиент лжёт/обойдён» — + свидетель назван (мутация «удаление разбора `too_large`»), третий столбец не + пуст. +- **AC6** — «одно число» (см. предупреждение о числах в §8 гейтов) закрыто + явным тестом, читающим оба источника плюс оба USER-GUIDE — ровно то, что + требует правило «одно число — один источник». +- **AC7/AC8** — паритет и «общий хелпер» доказываются существующими тестами + + ревью кода с явной пометкой «проверено чтением, не исполнением» ожидается на + этапе код-ревью — здесь это корректно поставлено в план, не выдано за + готовый факт. + +Ни одного места, где решение приложения выдано за факт без пометки: восемь +пунктов «принято предположительно» покрывают именно то, что пользователь не +наблюдает (выбор варианта (б) обоснован в аналитике и в самом ТЗ, а не +голословен) либо являются реализационными деталями (URL, буферизация, +именование). Продуктовых вопросов владельцу не задано и не нужно было — +формулировка «что человек увидит до/после» отвечает на оба вопроса, которые +вправе решать только он. + +## Находки + +### Low-1 — «новый необязательный параметр» уже существует +`docs/reviews` н/п. **Файл:** `src/backdrop-pick.ts:157-163` (сигнатура +`renderBackdropGuard`), уже используется декором в +`houseplan-editor-runtime.ts:7949`. +ТЗ (раздел «Не-скоуп») формулирует: «Общий `renderBackdropGuard` меняется +только добавлением необязательного параметра; поведение декора прежнее» — +подразумевая, что параметр `allowOriginal` появится этой задачей. Он уже +существует и уже используется тем же декором, на который ТЗ ссылается как на +прецедент. Значит для скрытия кнопки «Оставить оригинал» этой задаче не нужно +менять сигнатуру вовсе — только передать шестой аргумент из вызова для плана. +Действительно новым остаётся только параметр, переключающий ТЕЛО диалога +(`over_limit_body` vs `large_body`), который в разделе выше назван отдельно и +верно. Не блокирует ни один AC и не меняет объём работы по сути — это неточная +атрибуция «что уже есть» vs «что появится», сама работа от этого не меняется. +Правится словом при следующей правке ТЗ или снимается с записью — не +возвращает задачу на цикл. + +### Наблюдение (не находка, не блокирует) +Случай «растр больше предела **и** проба `hard`» ТЗ явно не разбирает (AC3 +условие «с читаемыми размерами» его обходит стороной). Сегодняшний код и без +этой задачи не даёт в `hard`-случае ни одной кнопки действия, кроме «Отмена» +(`renderBackdropGuard`: `${hard ? null : …}`) — то есть пользователь и сейчас +не может загрузить/уменьшить `hard`-файл, независимо от байтового предела. +Задача не ухудшает этот путь и не обязана его чинить (не про размер в байтах, +а про безопасность декодирования — чужой скоуп). Упоминаю, чтобы будущий +ревьюер кода не считал это пропущенным AC. + +### Трек `small` — соразмерность (не находка) +Задача добавляет новый аутентифицированный write-эндпоинт — обычно повод для +внимательности при выборе лёгкого трека. Аналитический комментарий явно прошёл +все пять критериев §5 и обосновал каждый (сложность 3 — копия двух уже +существующих шаблонов view почти дословно; риск закрыт паритетом проверок с +`ws_plan_set` и общим writer'ом; одна поверхность — один диалог, один +эндпоинт; нет миграции/UX-контракта/перф/touch-влияния). Сверка кода в этом +документе подтверждает, что описанные шаблоны (`HouseplanDecorAssetUploadView`, +`HouseplanUploadView`, `may_write`, `check_quota`, `atomic_write`) существуют +и совпадают буквально — обоснование не голословно. Оставляю как отмеченное, а +не как находку: критерии §5 не про «есть ли новый эндпоинт», а про +сложность/риск/поверхность/миграцию/UX/перф-touch, и все пять по существу +закрыты. + +## Что проверено и корректно + +- Все обязательные разделы §7.1 присутствуют, DoR §2.5 выполним по тексту ТЗ. +- Каждый явный технический факт о существующем коде, на который опирается + контракт, сверен построчно (таблица выше) — ни одной догадки, выданной за + факт. +- AC1–AC8 пронумерованы, однозначны, у каждого назван способ доказательства; + защитные AC несут третий столбец «чем краснеет» с конкретной мутацией. +- «Одно число» (`MAX_PLAN_BYTES`) имеет план проверки на трёх источниках сразу + (TS, Python, оба USER-GUIDE) — правило §8 учтено на стадии ТЗ, а не оставлено + коду. +- i18n: ключи ru/en перечислены дословно, de/fr делегированы с сохранением + плейсхолдеров и стиля существующего `err.too_large`; мёртвых ключей не + вводится. +- Совместимость: WS `plan/set` не меняет контракт (не-скоуп явно), общий writer + для обоих путей записи специфицирован как единственная точка проверок. +- Откат тривиален и описан честно (файлы не мигрируют, revert коммита + достаточен). +- Touch/перф корректно обоснованы как «не затронуты» со ссылкой на + правильную персону/поверхность (сверено с `TOUCH-SUPPORT.md`). + +## Чего не проверял + +- Не проверял сами тесты/смоки — на стадии ТЗ их ещё нет; их придётся + перепроверить на код-ревью, включая «умеет ли тест падать» для каждого из + трёх заявленных мутантов. +- Не проверял, что `PLANS_DIR`, `MAX_PLANS_BYTES`, `MAX_PLANS_FILES` + (папочные квоты) действительно не нуждаются в правке для нового + write-пути — ТЗ утверждает переиспользование через общий writer, что снимает + вопрос конструктивно (не два места квот), но исполнение проверит код-ревью. +- Не гонял никаких CI/локальных гейтов — на этапе ТЗ они неприменимы (нет кода + для проверки; ссылка на них в самом ТЗ — план, а не отчёт об исполнении). +- Не оценивал производительность потоковой буферизации 8 МиБ в памяти под + параллельными аплоадами — тот же паттерн, что и у декора (2 МиБ), риск не + новый по порядку величины и явно назван в разделе «Риски» ТЗ. + +## Материал раунда + +``` +Issue: #617 +Repo: Matysh/houseplan-card +SHA рабочей копии (репозиторий, не материал ТЗ): 0f97e3e639c4eb06b7866b8c1c757d98e2c0a5f7 +ТЗ: тело issue #617, раздел «## ТЗ» +sha256(нормализованное тело issue, UTF-8, как получено `gh issue view 617 --json body`): 696d986cca3720a468ba776055d544cececd4713062b4c5e5b6e2e44ced87092 +Длина тела: 18220 байт +Аналитический комментарий (трек/оценка): Matysh, 2026-09-24T01:01:56Z +``` + +--- + +**Вердикт: зелёный · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 0** + +--- + + + +## Материал раунда + +- Ветка: `dev`, коммит `0f97e3e639c4` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `f2603b2998c17fd63d3bc1248b5e2be36e4b598f` + ``` + git log --all --format='%H %T' | grep f2603b2998c1 + ``` +- Тело issue: `f8d7f51e97db367fb59302863a5ab105d72a8aa34c7fa955fdc6ee29139973c6` +- Вердикт конвейера: `green` · High 0