Issue: #617 User-Visible: no
30 KiB
CODE-REVIEW-617-r2
- Issue: #617 «Загрузка плана > ~3 МиБ обрывает WebSocket без сообщения»
- ТЗ: тело 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-Lengthpreflight, разбор 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, назван во вводной к ревью |
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=<base>..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 на этом же SHA43b52826(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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
06503abc6ca4b8664d7e1aef1a37291452bb6411git log --all --format='%H %T' | grep 06503abc6ca4 - Тело issue:
6682a2131de842acea0a178f21d254b2d3c0fb88309a244451362a5bb4475514 - Вердикт конвейера:
green· High 0