Files
houseplan-card/docs/reviews/CODE-REVIEW-617-r2.md
T
claude[bot] 2b3959e55b
Проверка (CI) / Предполёт: документация, провенанс, процесс (push) Failing after 13m26s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Canceled after 0s
Проверка (CI) / HACS: валидация репозитория (push) Canceled after 0s
Проверка (CI) / Hassfest: манифест интеграции (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Canceled after 0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Canceled after 0s
Проверка (CI) / Смоки: все шарды зелёные (push) Canceled after 0s
Проверка (CI) / Golden-кадры против принятых эталонов (push) Canceled after 0s
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Canceled after 0s
Проверка (CI) / Геометрия: TS/Python parity исполнена (push) Canceled after 0s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Canceled after 0s
Проверка (CI) / Доказательство выполненных проверок (push) Canceled after 0s
Проверка (CI) / Классификация изменённых файлов (push) Failing after 3m35s
docs: review document for #617
Issue: #617
User-Visible: no
2026-09-24 17:36:41 +00:00

30 KiB
Raw Blame History

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-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, назван во вводной к ревью
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 на этом же 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