Files
houseplan-card/docs/reviews/CODE-REVIEW-39-r2.md
T
2026-08-29 07:23:36 +00:00

16 KiB
Raw Blame History

CODE-REVIEW-39-r2

  • Issue: #39 «[HP-UX-09] большие подложки»
  • ТЗ: docs/specs/039-large-backdrops.md, ревизия 4.1 (спек-ревью зелёное на r3)
  • Предыдущий раунд: docs/reviews/CODE-REVIEW-39-r1.md, вердикт жёлтый, заход r1, ревью-SHA 0d82ee0a (этот SHA отсутствует в текущей истории — см. ниже).
  • Ревью-SHA этого раунда: 7c45372c32e89f143845445fecd02936c261df5a (git rev-parse HEAD сверен непосредственно перед выводом вердикта)
  • Заход: r2 · блокирующих циклов до этого разбора: 1/4 (трек — полный, лимит 4; зелёный вердикт r1 не образовал бы цикл, но r1 был жёлтым — цикл потрачен)
  • Диапазон дельты: git diff 7c31725a..7c45372c (см. примечание про SHA ниже)

Примечание про несовпадающий SHA из r1

Документ r1 называет ревью-SHA 0d82ee0a, которого в текущей истории нет (git cat-file -t 0d82ee0a → fatal: Not a valid object name). Это находка по инструкции («SHA в вердикте не назван / не воспроизводится — тоже находка»), но не блокирующая: origin/dev сейчас указывает на 4d737747, и его прямой родитель — коммит 7c31725a («feat: warn about huge backdrops…»), который побайтово совпадает по содержанию с тем, что описывает r1 (тот же diffstat «50 files, +1331/-371», то же сообщение коммита, тот же список файлов). Иными словами: 7c31725a — это 0d82ee0a после переписывания (git commit hash сменился, дерево — нет), и ветка уже стоит на актуальном dev без дополнительного ребейза (git merge-base origin/dev 7c31725a == вершина origin/dev). Это не ребейз «на ушедший вперёд dev» в смысле §7.2 — код после 0d82ee0a не менялся, менялся только адрес коммита. Дельта этого раунда поэтому корректно считается как 7c31725a..7c45372c, что совпадает с единственным новым коммитом в git log --oneline origin/dev..HEAD (не считая docs-коммита публикации e84af749).

Скоуп дельты

Один продуктовый коммит 7c45372c поверх зелёного 7c31725a:

  • src/backdrop-pick.ts (+13/-4) — правки M1 и M2;
  • demo/smoke_backdrop_guard.mjs (+77) — новые сценарии для M1 и M3;
  • scripts/mutation-gate.mjs (+17) — новый мутант для M1, обновлён find существующего мутанта под изменившийся текст того же catch-блока;
  • docs/TESTING.md (+5) — чек-лист для EXIF-сценария;
  • docs/images/01-view-desktop.png + docs/images/screenshots.json — пересчитан отпечаток документации (класс D/автоген, ожидаемо после правки src/**);
  • docs/reviews/CODE-REVIEW-39-r1.md — сам документ r1 (класс C, добавлен шагом публикации, не автором).

Геометрии, custom_components/**/*.py, конфиг-миграций дельта не касается — model-invariants и backend-гейт по-прежнему не относятся к этому диффу.

Как проверялось (гейты)

Гейт Команда Результат
typecheck npx tsc --noEmit чисто
unit npm test # tests 1523 / pass 1522 / fail 0 / skipped 1 (было 1518/1517 на r1; разница объясняется новым мутантом — test/mutation-gate.test.mjs итерирует реестр MUTANT_DEFINITIONS и генерирует по нему подтесты, отдельный тестовый файл руками не трогали)
build + bundle sync npm run build → cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js + diff -rq dist/houseplan-assets custom_components/houseplan/frontend/houseplan-assets побайтово совпадают; git status после сборки чист — коммит уже содержит пересобранный бандл
bundle budget npm run bundle:budget initial View 273 385 / 282 000 Б gzip, запас 8 615 Б
docs fingerprint node scripts/check-docs.mjs (дельта трогает src/**) «Documentation checks passed (7 files, 10 external links)»
smoke-select (по дельте) node scripts/smoke-select.mjs --base 7c31725a --head HEAD 1 «прямое совпадение»: smoke_backdrop_guard.mjs (← _backdropGuard); других смоков дельта не задевает — локализована в backdrop-pick.ts
smoke (адресный, из выборки) node demo/smoke_backdrop_guard.mjs все 27 ассертов зелёные, включая новые busyDismissIgnored, staleFlowNeverApplies, exifFixtureRotates, exifProbeReadsSof, exifReduceApplied, exifOptionPassed, exifReducedRotated; плюс переподтверждены не тронутые дельтой phase2RejectToast/phase2RejectCleanStaging/phase2TimeoutToast/phase2CleanStaging (AC4б)
mutation gate (адресно, новый id) node scripts/mutation-gate.mjs --id=backdrop-busy-dismiss-races-decision «тест покраснел, как обязан»; рабочее дерево чистое после прогона (воркчасть временная)

Не прогонял и почему

  • полную матрицу 201 смока — предрелизная обязанность, не гейт ревью; smoke-select по дельте называет только один прямой смок, дельта локальна;
  • npm run golden:verify, python -m pytest tests_backend, node scripts/model-invariants.mjs, perf-профили — дельта не трогает рендерер/геометрию/custom_components/**/*.py/перф-пути; то же обоснование, что в r1, дельта его не меняет;
  • demo/benchmark_backdrop_decode.mjs — калибровочный скрипт, не гейт, не тронут дельтой.

Закрытие раунда r1

Находка r1 Чем закрыта Где это видно
M1 — отмена гард-диалога во время busy не отменяет применение dismiss() теперь игнорирует Escape/скрим/Cancel, пока busy; original()/reduced() (обе ветки — успех и catch) дополнительно проверяют stillCurrent() перед apply(), так что даже принудительно снесённый гард не подставит устаревший результат src/backdrop-pick.ts:179-186 (dismiss, stillCurrent), :190 (original), :201 и в catch-блоке (reduced); новый мутант backdrop-busy-dismiss-races-decision в scripts/mutation-gate.mjs красит smoke_backdrop_guard.mjs, прогнан и подтверждён; смок-ассерты busyDismissIgnored/staleFlowNeverApplies — оба true в реальном прогоне
M2 — «одно число, один источник» нарушено для HARD_DIMENSION Литерал 16384 заменён на импортированную HARD_DIMENSION из backdrop-probe.ts — дублирования числа больше не существует физически, рекалибровка снова правка одного файла src/backdrop-pick.ts:10 (импорт), :171 (limit: HARD_DIMENSION)
M3 — AC8 не доказан ни тестом, ни записью Новый смок-блок собирает настоящий JPEG с APP1/EXIF (orientation 6), проверяет, что probe читает неповёрнутые SOF-размеры (8200×4100), что createImageBitmap реально получает imageOrientation:'from-image' (перехвачено хуком), и что итоговая уменьшенная копия — портрет 2048×4096, а не пейзаж demo/smoke_backdrop_guard.mjs (блок «AC8: EXIF-ориентация»); docs/TESTING.md — новая строка чек-листа со ссылкой [auto: smoke_backdrop_guard]; прогнан, exifProbeReadsSof/exifOptionPassed/exifReducedRotated — все true

Все три Medium-находки r1 закрыты кодом и тестом, не только заявлением автора.

Унаследовано из r1

Без повторной проверки принято (документ docs/reviews/CODE-REVIEW-39-r1.md, SHA 0d82ee0a / содержимое идентично 7c31725a, см. примечание выше) — дельта этих доказательств не касается:

  • AC1 (warn до decode), AC2 (уменьшенная копия 4096/aspect/alpha), AC5 (SVG байпас), AC6 (битые заголовки → unknown) — код, который их доказывает (backdrop-probe.ts, ветки classifyPlanFile/renderBackdropGuard вне тронутых строк), дельтой не задет;
  • AC7 (транзакция не поменялась) — доказывается нетронутым demo/smoke_plan_upload_reject.mjs, дельта его не касается;
  • AC3/AC9 (байт-в-байт паритет «оригинала» и FileReader) — доказаны смоком b64ParityWithLegacyLoop, не юнитом; расхождение со словом «юнит» в ТЗ снято в r1 как Low без правки, дельта эту часть не трогает;
  • построчный разбор backdrop-probe.ts на безопасность (границы typed array, ограничение циклов, PLAUSIBLE_MAX_SIDE, WebP VP8X) — файл не входит в дельту;
  • согласованность обоих рантаймов (houseplan-editor-runtime.ts, houseplan-onboarding-runtime.ts) с одним classifyPlanFile — не тронуты;
  • i18n-паритет en/ru/de, бюджет бандла (initial View, где i18n-словарь гарда не в eager-графе View) — числа пересчитаны в этом раунде (см. таблицу гейтов) и совпадают по порядку величины с r1, отдельно не переразбирались;
  • трейлеры исходного коммита 7c31725a (Issue: #39, User-Visible: yes, оба changelog в том же коммите) — не тронуты этим раундом.

AC4б (честная фаза 2 — reject/hang → тост, чистый staging) формально задета дельтой (правка внутри reduced()), поэтому передоказана заново в этом раунде, а не унаследована молча — см. таблицу гейтов (phase2RejectToast/phase2RejectCleanStaging/phase2TimeoutToast/ phase2CleanStaging, все true).

AC8 передоказан заново (был «не доказано» в r1, теперь «доказано») — см. таблицу «Закрытие раунда r1», M3.

Находки этого раунда

Новых High и Medium нет.

L1 — два из трёх новых stillCurrent()-чеков не имеют собственного мутанта

Файл: src/backdrop-pick.ts:190 (original()) и :201 (успешная ветка reduced(), до входа в catch).

Зарегистрированный мутант backdrop-busy-dismiss-races-decision бьёт только по busy-гейту в dismiss(). Существующий мутант, чей find обновили под новую строку, целится в catch-ветку reduced() (проверяет другое — что там нет молчаливого отката к оригиналу, а не что там есть stillCurrent()) — но по факту его текст теперь включает и новую строку stillCurrent(), так что эта ветка фактически покрыта смоком: staleFlowNeverApplies специально форсирует _backdropGuard = null перед тем, как «зависший» decode обязан упасть по таймауту и попасть в catch — я прочитал downscaleBackdrop (src/backdrop-probe.ts импорт, Promise.race с setTimeout(...reject('decode timeout')), src/backdrop-pick.ts:115-120) и подтвердил, что режим hang + __HP_BACKDROP_TIMEOUT_MS гарантированно уходит в catch, никогда в успех — то есть staleFlowNeverApplies реально проверяет именно catch-ветку stillCurrent().

Непроверенными остаются ровно два места: stillCurrent() в original() (строка 190) и в успешной ветке reduced() (строка 201, между encodePlanFile и apply) — ни один текущий сценарий не форсирует гард в null во время настоящего (не зависающего) decode/encode, так что при разрешении промиса код всегда попадает в них с stillCurrent() === true. Прочитал обе строки: логика идентична уже проверенной (host._backdropGuard ?.file === guard.file), копипаст-ошибки не вижу — оцениваю риск как низкий.

Не блокирует (Low, не Medium): поведенческого дефекта не нашёл, это пробел в глубине доказательства, а не в коде. Не правлю и не завожу отдельной находкой правку — фиксирую с запиской для истории; если решится чинить, самый дешёвый путь — расширить smoke_backdrop_guard.mjs сценарием «клик Keep original → форс-снести гард → убедиться, что planFile не проставился», аналогично уже существующему для reduced().

Вердикт

Три Medium из r1 закрыты кодом и подтверждены прогоном (не заявлением автора): M1 — новым мутантом и двумя смок-ассертами, M2 — устранением самого дублирования числа, M3 — новым end-to-end EXIF-сценарием. Новых High/Medium нет. Одна Low-находка (L1) — пробел в глубине покрытия двух из трёх защитных строк, не поведенческий дефект — зафиксирована без правки. Все дешёвые гейты и адресованные тяжёлые гейты (smoke по выборке, мутация по новому id) прогнаны лично на SHA 7c45372c и зелёные.

Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0