From 2b3959e55bcb139b6ad872a312ad212de36f44db Mon Sep 17 00:00:00 2001
From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2026 17:36:41 +0000
Subject: [PATCH] docs: review document for #617
Issue: #617
User-Visible: no
---
docs/reviews/CODE-REVIEW-617-r2.md | 316 +++++++++++++++++++++++++++++
docs/reviews/INDEX.md | 3 +-
2 files changed, 318 insertions(+), 1 deletion(-)
create mode 100644 docs/reviews/CODE-REVIEW-617-r2.md
diff --git a/docs/reviews/CODE-REVIEW-617-r2.md b/docs/reviews/CODE-REVIEW-617-r2.md
new file mode 100644
index 00000000..5233e045
--- /dev/null
+++ b/docs/reviews/CODE-REVIEW-617-r2.md
@@ -0,0 +1,316 @@
+# CODE-REVIEW-617-r2
+
+- Issue: [#617 «Загрузка плана > ~3 МиБ обрывает WebSocket без сообщения»](https://github.com/Matysh/houseplan-card/issues/617)
+- ТЗ: тело issue #617, раздел `## ТЗ` (файла в `docs/specs/` нет — решение #517)
+- Спек-ревью: `SPEC-REVIEW-617-r1.md`, зелёный, High 0 / Medium 0
+- Код-ревью r1: `CODE-REVIEW-617-r1.md`, зелёный, High 0 / Medium 0, все 8 AC доказаны
+- Ревью-SHA этого раунда: `43b52826e4259bf7be04d9dea07c9f36b3127221` (сверен
+ `git rev-parse HEAD` перед выводом вердикта), дерево `06503abc6ca4b8664d7e1aef1a37291452bb6411`
+- База диапазона: `origin/dev` = `f8e089bdf2bca59340eea0395b159454e319f8d7`
+- Заход: **r2** · блокирующих циклов израсходовано **0 из 2** (лёгкий трек, `small`)
+- Материал: `git log --oneline origin/dev..HEAD` — 4 коммита: `857369b1` (fix,
+ класс A/B, `User-Visible: yes`), `3fafd9f2` (test, класс B, `User-Visible: no`),
+ `9f3511f2` (docs: публикация `CODE-REVIEW-617-r1.md`), `43b52826` (build:
+ ребейз, `User-Visible: no`)
+
+## Почему это r2, а не продолжение зелёного r1
+
+Код-ревью r1 (SHA `e5305cff67d7…`) было зелёным и не должно было открывать
+новый цикл (§4). Слияние отказало на конфликте с ушедшим вперёд `dev`
+(issue-комментарии от `2026-09-24T…`): «Код-ревью зелёное — вердикт выше в
+силе, переделывать работу не нужно. Не удалось только слияние». Задача
+вернулась автору не на правку кода, а на ребейз; после ребейза «это другой
+код, и принимать его без проверки нельзя» (§2.10) — отсюда новый заход
+код-ревью, номер r2, бюджет считается по этапу отдельно от ревью ТЗ.
+
+## Важная процедурная находка (унаследована из r1, не блокирует)
+
+r1 уже фиксировал, что тело issue менялось после зелёного спек-ревью
+(хеш `f8d7f51e…` → `696d986c…`). Проверил, не изменилось ли тело ещё раз
+между код-ревью r1 и этим раундом:
+
+```
+sha256(gh issue view 617 --json body -q '.body') = d67ce50bd2746fd0b060f6a11f6917247944aa06e5d21a848bee797833f74c34 (27143 байта)
+sha256(первые 27142 байта того же текста) = 696d986cca3720a468ba776055d544cececd4713062b4c5e5b6e2e44ced87092 (совпадает с r1)
+```
+
+Разница — ровно один добавленный завершающий байт (`\n`) после уже
+финального `\n`. Содержательного изменения ТЗ нет: AC1–AC8 и весь текст,
+который проверял r1, идентичны байт-в-байт. Не завожу это отдельной
+находкой второй раз — тот же вывод, что и в r1: реализация соответствует
+финальному тексту ТЗ.
+
+## Дельта r1 → r2: ребейз, не правка
+
+По инструкции разбор остаётся полным, если дельта — «ребейз на ушедший
+вперёд dev» (именно этот случай). Ниже — как я убедился, что дельта не
+меняет смысл, а не принял слово автора хендоффа.
+
+**1. Коммиты `857369b1`/`3fafd9f2` — тот же патч, что и в r1, просто с новым
+родителем.** Сравнил список файлов и текст коммит-сообщений с r1
+(`CODE-REVIEW-617-r1.md`, коммиты `f2c9321a`/`e5305cff`): дословно те же
+файлы (`http_api.py`, `plans.py`, `websocket_api.py`, `backdrop-pick.ts`,
+оба runtime, i18n×4, тесты, смоки), то же описание правки multipart-теста
+(«теперь случай "multipart без части file" задаётся явно… тело без
+multipart… ждёт `bad_request`»). Хеши коммитов другие — ожидаемо при
+ребейзе.
+
+**2. Криптографическая проверка неизменности продуктового кода.** Автор
+хендоффа (`2026-09-24T17:13:11Z`) назвал дерево `06503abc6ca4…` и блобы
+пяти ключевых файлов. Я не поверил на слово — пересчитал сам:
+
+| Файл | Блоб на `HEAD` | Заявлено в хендоффе |
+|---|---|---|
+| `src/backdrop-pick.ts` | `c36f98fa60a7…` | совпадает |
+| `src/houseplan-onboarding-runtime.ts` | `18d7a4aa3279…` | совпадает |
+| `src/houseplan-editor-runtime.ts` | `99d6ca83e41f…` | совпадает |
+| `custom_components/houseplan/http_api.py` | `a5bc56080b60…` | совпадает |
+| `custom_components/houseplan/plans.py` | `7d17d5d17b0f…` | совпадает |
+
+`git log --format=%T -1 HEAD` = `06503abc6ca4…` — дерево тоже совпадает.
+Это не признание автора, это математический факт: если бы код отличался,
+хеш блоба был бы другим.
+
+**3. Полный список файлов, изменённых во всём диапазоне
+`origin/dev...HEAD` вне `dist/**`/`houseplan-assets/**` (сборка),
+совпадает построчно со списком r1** (`git diff origin/dev...HEAD --stat`
+по `src/**`, `custom_components/**`, `i18n/**`, `test/**`,
+`tests_backend/**`, `docs/**`, `scripts/**`, `demo/**`) — 66 файлов, те же
+имена и те же количества изменённых строк (`http_api.py` +98, `plans.py`
++60, `websocket_api.py` +41, `backdrop-pick.ts` +151/−… и т.д.), плюс два
+файла, которых в r1 не было: `docs/reviews/CODE-REVIEW-617-r1.md` (публикация
+r1) и `docs/reviews/INDEX.md` (регенерация индекса). Никаких новых
+продуктовых файлов, никаких «случайно» изменённых соседних строк.
+
+**4. Коммит `43b52826` сам по себе — только сгенерированное.**
+`git show 43b52826 --stat --name-only`, за вычетом `dist/**` и
+`custom_components/houseplan/frontend/houseplan-assets/**` — единственный
+файл `scripts/monolith-baseline.json`. Никаких `.py`/`.ts` там нет, конфликт
+не трогал логику.
+
+**5. Проверил, что `dev` действительно продвинулся не только на #642/#627,
+как говорит коммит-сообщение, а шире** —
+`git log --oneline c8f9b5d3…(база r1)..origin/dev`: помимо #642
+(refactor: вынос диалога «Оптимизировать планы» — это диалог **геометрической**
+оптимизации плана редактора: `preflightDiagnostics`, coincident-partition,
+near-axis — не имеет отношения к загрузке ФАЙЛА плана) и #627 (ленивые
+i18n-чанки `settings/support/topology` — другие namespace, не
+`toast.plan_too_large`/`backdrop.over_limit_body`), там же #623/#620/#631/#632/#643 —
+все CI/процесс/тест-инфраструктура (`scripts/task-packet.mjs`,
+`.github/workflows/*`, новые файлы `test/dialog-baseline.test.mjs`,
+`test/dialog-form-problems.test.mjs`, `scripts/rebase-generated.mjs`). Ни
+один не касается `src/backdrop-pick.ts`, `houseplan-editor-runtime.ts` в
+части плана, `http_api.py`, `plans.py`, `websocket_api.py` или
+`src/i18n/{en,ru,de,fr}.json`. Отсюда и бесконфликтный автомёрж, о котором
+сообщает хендофф, — не совпадение, а отсутствие пересечения зон правки.
+
+**Вывод:** дельта r1→r2 не меняет ни одной строки логики, которую проверял
+r1. Она меняет только сборку (новые хеши чанков, дерево `bundleBytes`
++1 698, `hostRefs` 4 869→4 872 — тот же относительный +3, что был принят на
+r1, только на новой абсолютной базе `dev`). Дальше в этом документе я
+заново читаю код и таблицу AC своими глазами (не копирую r1 без проверки),
+но не переоткрываю то, что уже математически доказано неизменным.
+
+## Закрытие раунда r1
+
+r1 не оставил находок High/Medium — закрывать нечего. Единственный пункт r1
+(процедурная заметка о более раннем расхождении хеша тела issue) не был
+находкой кода и не требовал действия; см. раздел выше — актуальность
+подтверждена повторно на этом SHA.
+
+| Из r1 | Статус на r2 |
+|---|---|
+| High/Medium в скоупе | нет (0/0 в r1) |
+| Low в скоупе | нет |
+| Процедурная заметка (хеш тела issue) | переподтверждена: текущий хеш = хешу r1 + 1 байт перевода строки, содержимого не меняет |
+
+## Унаследовано из r1 (без повторного личного прогона в этом раунде)
+
+Продуктовый код байт-в-байт идентичен (раздел выше), поэтому следующие
+результаты r1 переносятся как валидные для `43b52826`, а не перепрогонялись
+мной лично в этом раунде — но каждый из них независимо ПОДТВЕРЖДЁН зелёным
+прогоном CI на самом SHA `43b52826` (см. таблицу гейтов ниже), а не принят
+только на слово r1:
+
+- Личный прогон 5 мутантов `plan-upload-*` (r1, `CODE-REVIEW-617-r1.md`,
+ таблица гейтов) — все «поймано 1/1». На `43b52826` их прогнал шардированный
+ `mutation-gate.mjs --changed` в CI (см. ниже) и независимо ещё раз лично
+ автор хендоффа.
+- Личный прогон `smoke_plan_upload_limit/reject/race`, `smoke_backdrop_guard`,
+ `smoke_decor_images` (r1) — код, который они проверяют, не менялся;
+ `smoke_backdrop_guard`/`smoke_plan_upload_limit` дополнительно
+ переисполнены CI как «чистый гвард» перед каждой мутацией (см. §«Как
+ проверялось», пункт про `runCleanGuards`).
+- Разбор HA-веток чтением (`http_api.py:388-436`, AC4) — файл не менялся,
+ разбор остаётся в силе.
+- «Одно число» (AC6), общий writer (AC7), общий хелпер (AC8) — код-путь не
+ менялся, вывод r1 остаётся в силе; перечитал сам (см. ниже) и подтверждаю.
+
+## Как проверялось в этом раунде
+
+Прочитано: `docs/SCOPE.md` (J4/J6, лок-инвариант — не задет),
+`docs/process/REVIEWER.md`, `AGENTS.md`, тело issue #617 целиком (комментарии
+включая оба хендоффа ребейза и решение о возврате в `S6`), `CODE-REVIEW-617-r1.md`,
+`SPEC-REVIEW-617-r1.md`, `docs/USER-GUIDE.ru.md` (термины «План», «8 МБ» не
+изменились).
+
+Лично перечитан код (не только сверка хешей из раздела «Дельта»):
+- `custom_components/houseplan/http_api.py:372-448` (`HouseplanPlanUploadView`
+ целиком — авторизация, `Content-Length` preflight, разбор multipart,
+ порядок проверок, `store_plan_upload` под `upload_lock`, коды ответов) —
+ совпадает с описанием AC4 в ТЗ и с r1.
+- `src/houseplan-editor-runtime.ts:8087-8246` (`_saveSpaceDialog` целиком,
+ включая `catch`) — подтверждено лично: `uploadPlanFile` вызывается ДО
+ правки `cfg`/`sp`, `catch` НЕ обнуляет `this.host._spaceDialog` (диалог
+ остаётся открытым) и не вызывает `_saveConfigNow`/`config/set` повторно —
+ ровно то, что требует AC5.
+- `src/houseplan-editor-runtime.ts:221,7948,8110` — импорт и оба вызова
+ `stagePlanFile`/`uploadPlanFile` из `backdrop-pick.ts` присутствуют.
+- `scripts/mutation-registry.mjs:3512-3570` — все 5 записей `plan-upload-*`
+ на месте, их `find`-паттерны синтаксически совпадают с текущим
+ `backdrop-pick.ts`/`plans.py` (иначе `applyPatches` не нашёл бы совпадение
+ и CI-шард упал бы с ошибкой подготовки, а не «поймано» — этого не
+ случилось, все 6 шардов зелёные).
+- `git grep '<<<<<<<\|=======\|>>>>>>>'` по `src/**`/`custom_components/**` —
+ ложных срабатываний нет (только декоративные `// =====` разделители
+ секций), настоящих следов незакрытого конфликта нет.
+
+### Гейты
+
+| Гейт | Результат | Как |
+|---|---|---|
+| Validate CI на `43b52826` (общий статус) | **success** | [run 36032869059](https://github.com/Matysh/houseplan-card/actions/runs/36032869059), назван во вводной к ревью |
+| `npx tsc --noEmit`, `npm test`, `npm run build` + сверка копий бандла | ok | job «Фронтенд: типы, юниты, мутанты, синхрон бандла» в 36032869059, зелёный — не перегонял лично, дешёвые гейты приняты по инструкции |
+| `node scripts/check-docs.mjs` | ok | job «Предполёт: документация, провенанс, процесс», зелёный |
+| `npm run lint:unused` (связность монолита) | ok | тот же job «Фронтенд…»; числа монолита сошлись с новой базой `dev` |
+| `node scripts/no-new-any.mjs` | ok | тот же job |
+| `npm run bundle:budget` | ok (предупреждение о запасе 11 508 < 15 000 — известный несвязанный долг #367/#474, было и до ребейза) | тот же job |
+| `node scripts/mutation-gate.mjs --changed=..HEAD` (диффовые мутанты, 6 шардов) | **ok, 6/6 зелёные** | jobs «Мутанты по диффу (1…6/6)» в 36032869059; каждый шард сначала гоняет `runCleanGuards` — чистый прогон гварда на пересобранном дереве ДО мутации, затем патчит и требует красного — это и есть личный (не по заявлению автора) прогон гвардов `smoke_plan_upload_limit.mjs`, юнита `plan-upload-limit.test.mjs`, `pytest tests_backend/test_plan_upload.py` на этом SHA, устроенный самим CI, а не мной вручную |
+| `python -m pytest` в HA-харнессе | ok | job «Бэкенд: pytest в Home Assistant», зелёный |
+| Смоки полного набора / golden / perf-смок / геометрия TS↔Python | **не гонялись в этом Validate-прогоне** (jobs показывают `-`/0s) | по дизайну: полный набор смоков/golden/perf в Validate условен на входе `heavy` (флаг «полный набор» у `workflow_dispatch`, см. `.github/workflows/validate.yml:817-822`), в этом прогоне не запрошен — это стандартное поведение лёгкого Validate, не пропуск с моей стороны |
+| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 46 прямых совпадений, все 46 прогнаны | не мной лично в этом раунде — приложено к хендоффу автора (`2026-09-24T17:13:11Z`), результат `exit 0`; полагаюсь на него, потому что код, который эти смоки проверяют, доказанно (раздел «Дельта») идентичен коду, на котором те же смоки прогнал лично r1 (`smoke_plan_upload_limit/reject/race`, `smoke_backdrop_guard`) — не слепое доверие, а перенос уже проверенного результата на доказанно тот же код |
+| `node scripts/mutation-gate.mjs --id=plan-upload-*` (5 мутантов поимённо) | ok, 5/5 поймано | автор хендоффа лично; независимо продублировано CI-шардами (диффовый прогон тех же ID, см. выше) |
+| `npm run invariants` | не применимо | геометрия не менялась ни в одном коммите диапазона |
+| performance-профили | не применимо | AC их не называет |
+
+Я не перегонял тяжёлые гейты (полный `npm test`, полный HA pytest, полный
+мутация-реестр, полный смок-набор, golden) лично на своей машине —
+принимаю их по зелёному CI на этом самом SHA, что и предписывает вводная
+инструкция («Дешёвые гейты на этом SHA уже подтверждены»). Дополнительно (и
+это не предписано, а моя личная проверка) я убедился, что диффовые
+мутация-шарды CI реально включали гварды этой задачи: гварды `plan-upload-*`
+— это ровно `smoke_plan_upload_limit.mjs`, юнит-тест и `pytest
+test_plan_upload.py`, а `runCleanGuards` в `scripts/mutation-execution.mjs:202`
+гоняет каждый такой гвард на пересобранном чистом дереве ДО мутации и
+проваливает гейт (`return 2`), если гвард не зелёный — то есть зелёные 6/6
+шардов математически means эти три гварда прошли на `43b52826`, а не только
+на дереве r1.
+
+## Разбор критериев приёмки (текущий текст ТЗ, идентичен тексту, проверенному r1)
+
+| AC | Разбор на этом SHA | Вердикт |
+|---|---|---|
+| AC1 (5 МиБ → один POST, `plan_url` из ответа, соединение живо) | Код идентичен r1 (раздел «Дельта», блобы совпали). `smoke_plan_upload_limit.mjs`/юнит — тот же гвард, зелёный на этом дереве и как «чистый» перед каждой из 6 диффовых мутаций CI. | **Доказан** (перенос доказательства r1 на доказанно идентичный код) |
+| AC2 (SVG `MAX+1` → тост, без запросов) | `src/backdrop-pick.ts` идентичен блобом r1; мутант `plan-upload-client-limit` красный — подтверждено CI-шардом (`runCleanGuards` + мутация). | **Доказан, защитный AC подтверждён мутацией на этом SHA** |
+| AC3 (растр `MAX+1` → диалог без «Оставить оригинал», уменьшенная копия `>` предела → тост) | Тот же файл, те же строки (`renderBackdropGuard` вызов с `size <= MAX_PLAN_BYTES`). Мутанты `plan-upload-guard-original`, `plan-upload-reduced-over-limit-staged` — красные на CI. | **Доказан, обе ветки подтверждены мутацией на этом SHA** |
+| AC4 (сервер: граница включительная, коды ответов) | Лично перечитал `http_api.py:372-448` на `43b52826` — порядок проверок, коды и `507`/`413`/`400×3` совпадают с ТЗ построчно. Мутант `plan-upload-server-bound` красный на CI; HA-специфичные ветки (403/`may_write`, реальный multipart) — «проверено чтением, не исполнением» + зелёный `pytest в Home Assistant` job на этом SHA. | **Доказан** (чтением для HA-веток, мутацией+CI для границы) |
+| AC5 (413 → текст с пределом, диалог открыт, `config/set` не отправлялся) | Лично перечитал `_saveSpaceDialog`/`catch` (`houseplan-editor-runtime.ts:8087-8246`) на `43b52826`: `uploadPlanFile` до правки конфига, `catch` не закрывает диалог и не вызывает сохранение повторно. Мутант `plan-upload-413-text-dropped` красный на CI. | **Доказан, подтверждён и личным чтением, и мутацией на этом SHA** |
+| AC6 (одно число TS/Python/оба USER-GUIDE) | Файлы-источники (`validation.py`, `docs/USER-GUIDE*.md`) вне диапазона правок #617 в этом ребейзе, не менялись; юнит-тест, сверяющий все 4 источника, зелёный (CI). | **Доказан** |
+| AC7 (WS не меняется, общий writer) | `websocket_api.py` идентичен блобом r1; `ws_plan_set` по-прежнему вызывает `store_plan_upload` без собственного `atomic_write`. | **Доказан** |
+| AC8 (оба рантайма — один хелпер) | Лично перепроверил оба места вызова (`houseplan-editor-runtime.ts:7948/8110`, импорт `:221`) — идентичны r1; `houseplan-onboarding-runtime.ts` не менялся (блоб совпал). | **Доказан** |
+
+## Проверено чтением и корректно (без отдельного личного прогона)
+
+- Единая авторизация (`may_write`) и единый набор `PLAN_EXTENSIONS` — файлы
+ не менялись со времён r1, разбор r1 остаётся в силе.
+- Копия-при-записи и отказ по квоте без следов — тот же код, тесты
+ `test_issue_617_store_plan_upload_is_copy_on_write` и
+ `..._quota_refusal_leaves_nothing` зелёные в CI (job «Бэкенд: pytest в Home
+ Assistant»).
+- Декор не задет: единственный вызов `renderBackdropGuard` из декор-пути не
+ передаёт `planLimitBytes` — код не менялся; `smoke_decor_images.mjs`
+ проверял это лично r1, код идентичен.
+- i18n: 4 языка, 2 новых ключа — файлы `src/i18n/*.json` не входят в diff
+ #627 (там другие namespace-файлы `settings/support/topology`), блобы не
+ менялись со времён r1.
+- Трейлеры: `857369b1` — `Issue: #617`, `User-Visible: yes`, оба changelog в
+ том же коммите (не изменилось); `3fafd9f2` — `User-Visible: no` (тест-онли);
+ `43b52826` — `Issue: #617`, `User-Visible: no` (только сборка/база
+ монолита, ни одной пользовательской строки не добавлено — корректная
+ разметка); `9f3511f2` — `User-Visible: no` (публикация ревью-документа,
+ тоже корректно).
+- «Одно число — один источник» (§8): дельта ребейза не вводит второго места
+ с числом «8» или «8388608» — единственная правка чисел в этом раунде это
+ `monolith-baseline.json` (техническая метрика связности, не
+ пользовательское число) и `bundle-budget`-замеры, оба объяснены в
+ commit-message и в полосе допуска.
+- `git rev-parse HEAD` = `43b52826e4259bf7be04d9dea07c9f36b3127221`, сверено
+ перед выводом вердикта; ветки/чекауты на другой SHA не делал (только
+ `git fetch origin dev`, что не двигает рабочую копию).
+
+## Чего не проверял
+
+- Полный `npm test`, полный HA pytest (`test_ha_*.py`), полный
+ мутация-реестр, полный набор смоков/golden/perf — не гонял лично; приняты
+ по зелёному Validate/CI на этом же SHA `43b52826` (`run 36032869059`).
+ Полные наборы — предрелизный гейт, не гейт ревью (§8).
+- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` — не гонял
+ сам; использую результат из хендоффа автора (46 прямых совпадений, все
+ прогнаны, `exit 0`), потому что независимо доказал (раздел «Дельта»), что
+ проверяемый этими смоками код на `43b52826` идентичен коду, на котором
+ r1 те же смоки уже прогнал лично.
+- Ручное тестирование в браузере — ручного тестирования в цикле нет
+ (инструкция); эквивалент — прогнанные CI и указанные выше личные прогоны
+ гвардов через диффовые мутация-шарды.
+- `npm run invariants`, performance-профили — не применимо, геометрия и
+ перф не задеты ни одним коммитом диапазона (как и в r1).
+- Не проверял вживую реальный внешний прокси перед HA (413 без тела) —
+ как и r1, только юнит-симуляцией; в этом раунде код `_pickPlanFile`/
+ `uploadPlanFile` не менялся, разбор r1 остаётся в силе.
+- Не проверял содержимое коммита `9f3511f2` (публикация `CODE-REVIEW-617-r1.md`
+ в `docs/reviews/`) на предмет полноты — это конвейерный шаг публикации
+ предыдущего вердикта, не продуктовый код и не предмет этого код-ревью.
+
+## Находки
+
+Нет находок High или Medium ни в скоупе, ни вне скоупа. Процедурная заметка
+о хеше тела issue унаследована из r1 и переподтверждена (см. выше) — не
+дефект кода, не открывает цикл.
+
+## Вердикт
+
+Дельта r1→r2 — исключительно технический ребейз: криптографически (сверка
+дерева и блобов пяти ключевых файлов) и построчно (список изменённых файлов
+всего диапазона) подтверждено, что продуктовый код, тесты, i18n и AC-текст
+не изменились ни на байт с момента зелёного `CODE-REVIEW-617-r1.md`.
+Коммиты `dev`, вошедшие при ребейзе (#642 — геометрический диалог
+«Оптимизировать планы», #627 — ленивые i18n-чанки другого namespace,
+#623/#620/#631/#632/#643 — CI/процесс/тест-инфраструктура), не пересекаются
+ни с одним файлом, который трогает #617 — этим объясняется бесконфликтный
+автомёрж. Единственное реальное изменение раунда — пересборка бандла и
+обновление базы монолита, оба в допустимой полосе и с трейлером
+`User-Visible: no`, что верно.
+
+Все 8 AC остаются доказанными: пять защитных AC (AC2, AC3×2, AC4, AC5)
+подтверждены не заявлением автора, а зелёными диффовыми мутация-шардами CI
+на этом самом SHA (`runCleanGuards` гоняет гвард на чистом дереве ДО
+мутации, затем требует красного после — оба факта видел в зелёном логе
+CI), плюс личным чтением ключевых мест (`http_api.py`, `_saveSpaceDialog`)
+на `43b52826`, а не по памяти о r1. Трейлеры и changelog в порядке, «одно
+число — один источник» не нарушено.
+
+**Вердикт: зелёный · заход r2 · блокирующих циклов 0/2 · High: 0 · Medium: 0**
+
+---
+
+
+
+## Материал раунда
+
+- Ветка: `issue/617-plan-http-upload`, коммит `43b52826e425` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
+- Дерево материала: `06503abc6ca4b8664d7e1aef1a37291452bb6411`
+ ```
+ git log --all --format='%H %T' | grep 06503abc6ca4
+ ```
+- Тело issue: `6682a2131de842acea0a178f21d254b2d3c0fb88309a244451362a5bb4475514`
+- Вердикт конвейера: `green` · High 0
diff --git a/docs/reviews/INDEX.md b/docs/reviews/INDEX.md
index d08e3e17..cf4e6821 100644
--- a/docs/reviews/INDEX.md
+++ b/docs/reviews/INDEX.md
@@ -1,6 +1,6 @@
# Индекс ревью
-Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 1034, issue: 365. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`.
+Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 1035, issue: 365. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`.
| Issue | Документ | Этап · раунд | Вердикт | H | M | Находки | Файлы |
|---|---|---|---|---:|---:|---|---|
@@ -49,6 +49,7 @@
| #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` |
| #617 | [CODE-REVIEW-617-r1.md](CODE-REVIEW-617-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — |
+| #617 | [CODE-REVIEW-617-r2.md](CODE-REVIEW-617-r2.md) | code · r2 | 🟢 зелёный | 0 | 0 | — | — |
| #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` |
| #615 | [SPEC-REVIEW-615-r2.md](SPEC-REVIEW-615-r2.md) | spec · r2 | 🟢 зелёный | 0 | 0 | — | — |
| #614 | [SPEC-REVIEW-614-r1.md](SPEC-REVIEW-614-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | — | — |
]