# SPEC-REVIEW-396-r2 - **Issue:** https://github.com/Matysh/houseplan-card/issues/396 - **ТЗ под ревью:** `docs/specs/396-camera-transition-fixes.md`, коммит `d5fd5926faab12ec0883458b1d4a932ea9344bb3` (HEAD, `docs: #396 spec revision 2 per SPEC-REVIEW-396-r1`) - **Этап:** spec (PROCESS.md §2.4) - **Трек:** обычный (без метки `small`/`trivial`) - **Заход:** r2 · блокирующих циклов ревью ТЗ израсходовано 1 из 4 (до этого вердикта) ## Скоуп ревью Раунд r1 вернул красный вердикт с одной High-находкой: раздел «(1) B1» ТЗ утверждал, что переход отменяют семь конкретных мест (`:1267,:1401,:1551,:4188,:4216,:6089,:6206`), а фактическая сверка показала десять мест плюс отдельный `.dispose()` на disconnect — при этом ни настоящий `_stagePointerDown` (`:6387`, буквальный сценарий issue), ни настоящий `_zoomAt` (`:6283`) в семёрке не фигурировали, а раздел «Скоуп» не называл `_stagePointerDown` затрагиваемой функцией. Владелец ответил ревизией 2: заменил список семи чисел таблицей из одиннадцати мест с классификацией «пользовательская/структурная», добавил `_stagePointerDown` в «Скоуп», переформулировал AC1/AC2. Это ревью — по дельте (PROCESS.md §2.9): единственный правленый раздел — «(1) B1» плюс «Скоуп/не-скоуп» плюс AC1/AC2 (см. диф ниже). B2, M2, AC3–AC7, UX, модель данных, i18n, риски, откат, release-артефакты дельту не задевают и наследуются из r1 без повторной проверки. Причина полного нового прочтения таблицы вместо точечной сверки одной строки: сама таблица — это ровно тот артефакт, для которого r1 объявил высокий риск фактической ошибки, и инструкция раунда прямо требует не доверять заявлению автора на слово, а сверять «строку кода или текста». ``` git diff 4e0a30a78161a4ff59679454d1f8b5c471e10008..HEAD -- docs/specs/396-camera-transition-fixes.md ``` (коммит `4e0a30a7` — HEAD ветки на момент вердикта r1). Продуктового кода в ветке по-прежнему нет: `git diff --stat 4e0a30a7..HEAD` показывает только `docs/reviews/SPEC-REVIEW-396-r1.md` (публикация прошлого вердикта) и `docs/specs/396-camera-transition-fixes.md`. `src/**` не тронут. ## Как проверялось 1. Прочитан весь диф правленых разделов ТЗ (раздел «(1) B1», «Скоуп/не-скоуп», AC1, AC2, строка «Ревизия»). 2. Каждая строка новой таблицы одиннадцати мест сверена с фактическим кодом на HEAD той же командой, что и в r1: `grep -n "_cancelCameraTransition(false)\|_cancelCameraTransition(true)\|_cameraTransition\.\(cancel\|dispose\)" src/houseplan-card.ts` — 14 строк (13 точек вызова + строка-определение самого `_cancelCameraTransition` на `:1242`, которая не точка вызова и в таблицу не входит). 3. Каждая из 13 точек вызова прочитана в контексте содержащей функции: `:1159` (`_onMotionChange`), `:1267` (`_startCameraTransition`, no-op ветка), `:1401` (`_cancelModeTransition`), `:1551` (`_commitSpace`), `:2363` (`_pageVisibility`), `:2745` (`disconnectedCallback` → `.dispose()` напрямую), `:4188` и `:4216` (обе — внутри `_adoptStructuralResponses`, `:4167-4224`), `:6089` (`_applyView`), `:6206` (`_refitView`), `:6283` (`_zoomAt`), `:6360` (`_restoreZoom`), `:6387` (`_stagePointerDown`). 4. Отдельно перепроверены оба вызывающих контекста `_zoomAt` (`:6517` — pinch-move, `:6897` — touch multitouch move): оба вызывают `_saveZoom()` сразу после `_zoomAt(...)` — заявление таблицы «правки не требует» верно. 5. Сверено, что `_stagePointerDown` (`:6387`) действительно первой строкой вызывает `_cancelCameraTransition(false)` и не содержит гарантированного `_saveZoom()` на пути простого клика — это подтверждает и контракт AC1, и то, что High-1 из r1 закрыт по существу (строка теперь в «Скоуп» и в AC1). 6. Гейты (`typecheck`/`test`/`build`) не прогонялись — продуктового кода нет (только doc-коммиты), это вне скоупа этапа spec (PROCESS.md §2.4/§8), как и в r1. ## Находки ### Medium-1 — таблица приписывает строки `:4188`/`:4216` несуществующей функции `_load` **Файл:** `docs/specs/396-camera-transition-fixes.md`, раздел «(1) B1», строка таблицы `| \`:4188\`, \`:4216\` \`_load\` | adoption конфига и layout | структурная |`. Фактическая проверка: в `src/houseplan-card.ts` идентификатора `_load` не существует (`grep -n "\b_load\b" src/houseplan-card.ts` — пусто). Обе строки `:4188` и `:4216` лежат внутри одной и той же функции `_adoptStructuralResponses` (`:4167-4224` — сигнатура на `:4167`, config-adopt ветка на `:4188`, layout-adopt ветка на `:4216`), которую вызывают `_loadFromServer` (`:4247`, вызов на `:4284`) и второй caller на `:4460` (config-only reload). Ни один из этих реальных идентификаторов не совпадает с `_load`: ближайшие по написанию — `_loadFromServer`, `_loadOk`, `_loading`, `_loadTries`, `_loadRetryTimer` — все разные функции/поля. Это ровно тот класс дефекта, который r1 квалифицировал как High (таблица называет несуществующее/неверное имя функции, хотя визуально оформлена как проверенный факт — «сверено `grep`'ом… каждое прочитано»), и он **новый**: в тексте r1 (и в собственном ответе ревьюера, и в разборе владельца) эти же строки корректно названы `_adoptStructuralResponses` — ревизия 2, поправляя High-1, сама внесла эту неточность взамен верного имени. **Почему Medium, а не High.** У этой строки нет требуемой правки кода — класс «структурная», и правка по контракту B1 нужна только в `_stagePointerDown` (`:6387`), которая в «Скоуп» названа явно и корректно. Ложное имя `_load` не рискует тем, что разработчик пропустит обязательную правку (в отличие от High-1 r1, где отсутствие `_stagePointerDown` в списке грозило оставить буквальный сценарий issue незачиненным) — оно рискует ввести в заблуждение читателя, сверяющего полноту классификации (найдёт `_loadFromServer`, не найдёт `_load`, потратит время на поиск несуществующего имени), и подрывает доверие к самому заявлению «каждое прочитано» в этой же таблице. **Что нужно поправить.** Заменить `_load` на `_adoptStructuralResponses` в строке таблицы для `:4188`/`:4216`. ### Low-1 (waived) — вторая функция в строке `:1159`/`:2363` не названа Строка таблицы `| \`:1159\` \`_onMotionChange\`, \`:2363\` | ... |` называет функцию только для первого числа; `:2363` фактически лежит в `_pageVisibility` (`:2357-2363`, поле-стрелка класса), но таблица этого не пишет. В отличие от Medium-1 здесь нет ложного утверждения — просто пропуск имени, классификация («`cancel(true)`, цель коммитится, уже сохраняется через `settled`») верна для обеих строк. Не блокирует; можно поправить попутно при правке Medium-1, а можно оставить — снимаю без требования правки. ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | High-1: список из семи чисел не совпадает с реальными вызовами; `_stagePointerDown` (`:6387`) и `_zoomAt` (`:6283`) отсутствуют в списке; «Скоуп» не называет `_stagePointerDown` | Список заменён таблицей из одиннадцати мест (все 13 точек вызова, включая `.dispose()` на `:2745`), с явной классификацией «пользовательская/структурная»; `_stagePointerDown` добавлен в «Скоуп» жирным и назван «буквальный сценарий issue» с явной пометкой «сохранять»; `_zoomAt` явно помечен «правки не требует» с обоснованием | `docs/specs/396-camera-transition-fixes.md`, раздел «(1) B1» (новая таблица) и раздел «Скоуп / не-скоуп» (`**\`_stagePointerDown\`**`); проверено построчной сверкой с кодом в этом раунде (см. «Как проверялось» п.2–5) — таблица теперь фактически верна за единственным исключением Medium-1 (имя `_load` вместо `_adoptStructuralResponses` для `:4188`/`:4216`) | | Риск «поймает только код-ревью, лишний цикл» | Снят тем же исправлением: `_stagePointerDown` теперь явно в скоупе и в AC1, реализация не сможет случайно пропустить это место, читая раздел «Скоуп» как границу работы | AC1: «После пользовательской отмены перехода (`_stagePointerDown` — касание плана...) сохранённый зум пространства равен показанному» | ## Унаследовано из r1 Не проверялось повторно в этом раунде — дельта их не касается, приняты по документу `docs/reviews/SPEC-REVIEW-396-r1.md` на коммите `4e0a30a78161a4ff59679454d1f8b5c471e10008`: - **B2 (якорь от отстающего кадра)** — контракт, математическое обоснование (`_cameraTargetAt` строит якорь от `_cameraState()`, presented, независимо от `target`), правка §10 `docs/specs/082-smooth-zoom.md` (AC6) — раздел «(2) B2» ТЗ не входит в диф r1→r2, не перечитывался. - **M2 (feather не заморожен)** — цитата `resolveGlowFeather(..., !this._pinchStart && !this._panStart)` (`houseplan-card.ts:10845-10847`) сверена дословно в r1; в этом раунде дополнительно подтверждено, что строка не сдвинулась (см. «Как проверялось», п. проверки M2 в контексте неизменного дифа) — раздел «(3) M2» ТЗ тоже вне дифа r1→r2. - **AC3–AC7** — формулировки не менялись (вне дифа), контракты и способы доказательства признаны верными в r1. - **Структура ТЗ / обязательные разделы PROCESS.md §7.1** — все на месте, проверено в r1 (сценарий, до/после, скоуп, UX, модель данных, i18n, план автотестов, риски, откат, release-артефакты). - **Правка §10/§13 `docs/specs/082-smooth-zoom.md` (AC6)** — обоснованность и то, что она не требует продуктового решения владельца, подтверждены в r1. - **Трек (обычный, не `small`)** — обоснован в r1 и не оспаривается. - **Открытых продуктовых вопросов владельцу нет** — подтверждено в r1; Medium-1 этого раунда — тоже чисто технический вопрос (имя функции в таблице), не продуктовый, решается вердиктом, а не владельцем. - **Мутанты названы конкретно** и подтверждают правило «тест умеет падать» — раздел «Мутанты» вне дифа r1→r2. ## Что проверено и корректно (в этом раунде, по дельте) - Новая таблица из одиннадцати мест фактически верна для 12 из 13 точек вызова (все, кроме `:4188`/`:4216`) — построчно сверено с кодом на HEAD. - `_stagePointerDown` корректно классифицирован как единственное пользовательское место, требующее правки, и корректно добавлен в «Скоуп». - `_zoomAt` корректно исключён из мест, требующих правки: оба его реальных вызывающих контекста (`:6517`, `:6897`) действительно вызывают `_saveZoom()` сразу после — проверено чтением обоих мест. - AC1 и AC2 переформулированы в соответствии с новой таблицей, без изменения сути контракта B1/B2/M2 из r1; AC2 корректно охватывает `_restoreZoom` как структурное место, которое ничего не пишет в `localStorage` (сам `_restoreZoom` и вызываемый им `_applyView` не содержат `_saveZoom()`). - Раздел «Ревизия» корректно ссылается на SPEC-REVIEW-396-r1 и называет High-1 явно — соответствует ожиданию «показать, чем именно закрыта находка предыдущего раунда» текстом, а не только заявлением. ## Чего не проверял - Разделы «(2) B2», «(3) M2», UX, модель данных/миграция, i18n, план автотестов, риски, откат, release-артефакты — не входят в диф r1→r2, унаследованы из r1 (см. раздел выше), повторно не перечитывались построчно против кода в этом раунде. - Гейты `typecheck`/`test`/`build`/`check-docs`/`invariants`/смоки/ `golden:verify` — неприменимы: продуктового кода в ветке нет (только doc-коммиты), как и в r1. - Содержимое внешнего аудита `AUDIT-2026-08-31-v1700beta1.md` — по-прежнему не в репозитории, не перепроверялось (не входит в дельту этого раунда). - `docs/specs/082-smooth-zoom.md` целиком — не перечитывался повторно в этом раунде, так как правки §10/§13 вне дифа r1→r2; принято на веру из r1. ## Вердикт Жёлтый. High: 0. Medium: 1 (Medium-1 — строка таблицы раздела «(1) B1» приписывает `:4188`/`:4216` несуществующей функции `_load` вместо фактической `_adoptStructuralResponses`; в скоупе этого же раунда, чинится в нём же, без отдельного issue). Low: 1, waived (пропуск имени функции для `:2363` — не блокирует, можно поправить попутно). High-1 из r1 закрыт по существу: `_stagePointerDown` (буквальный сценарий issue) теперь явно в «Скоуп» и в AC1, `_zoomAt` корректно исключён из мест, требующих правки, таблица одиннадцати мест фактически верна за единственным исключением Medium-1. После правки Medium-1 (заменить `_load` на `_adoptStructuralResponses` в одной строке таблицы) — заход по дельте (PROCESS.md §2.9): проверить только эту строку, всё остальное наследуется без повторной проверки. **Вердикт: жёлтый · заход r2 · блокирующих циклов 2/4 · High: 0 · Medium: 1 → в задаче · Документ: docs/reviews/SPEC-REVIEW-396-r2.md**