mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
168 lines
16 KiB
Markdown
168 lines
16 KiB
Markdown
# CODE-REVIEW-39-r2
|
||
|
||
- Issue: [#39 «[HP-UX-09] большие подложки»](https://github.com/Matysh/houseplan-card/issues/39)
|
||
- ТЗ: `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**
|