16 KiB
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, ревью-SHA0d82ee0a(этот 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