# SPEC-REVIEW-500-r2 - **Issue:** https://github.com/Matysh/houseplan-card/issues/500 - **Этап:** ревью ТЗ (PROCESS.md §2.4) - **Заход:** r2 · блокирующих циклов израсходовано 1 из 4 до этого раунда - **Материал:** ветка `issue/500-config-adoption-boundary`, коммит `c35ecf13206bb5037f86f1e55fc27b8ed46604ee` (docs-only поверх ревью-документа r1 `565a0c4a`), файл ТЗ `docs/specs/500-config-adoption-boundary.md`, дерево `9a354a9fa366fcca195773d22a15f7947caf5d14`, блоб ТЗ `5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9`. - **Ревьюер:** Claude (роль «ревьюер ТЗ», отдельно от автора). - **Предыдущий раунд:** `docs/reviews/SPEC-REVIEW-500-r1.md`, вердикт жёлтый, материал `ca07067386dee0ce2bfad64c169404c9284cd242` (докс-коммит поверх `origin/dev` = `6b31e945`). ## Скоуп ревью (по §2.10 — объём по дельте) Разбор по дельте: r1 нашёл две High-находки, обе — неполная инвентаризация кода, на которой построены AC. Автор ответил коммитом `c35ecf13` (поверх `ca070673`), диапазон изменений — только `docs/specs/500-config-adoption-boundary.md`. Дельта не локальна в смысле «одна строка»: она вводит новое понятие (два профиля adoption вместо одного) и переписывает §3, §4, §6.3, §6.4, AC1–AC4, §11, §12 — то есть проверке подлежит вся дельта диффа `ca070673..c35ecf13`, а не только формальное «закрыт ли пункт High-1/High-2». AC5–AC8, §7–10, §13–14 дельта не задевает — унаследованы из r1 без повторной проверки (раздел ниже). Как и в r1, факт-чек количественных и структурных утверждений сделан прямым чтением `src/**` на рабочей копии, соответствующей материалу (докс-only ветка поверх текущего `dev`, код идентичен тому, что описывает ТЗ). ## Как проверялось ``` grep -rn "_adoptStructuralResponses" src/ grep -rn "_serverCfg\s*=[^=]" src/ --include=*.ts # исключая === сравнения grep -rn "_layoutRev\s*=[^=]" src/ --include=*.ts grep -rn "_cfgContentFingerprint\s*=[^=]" src/ --include=*.ts grep -rn "_layoutContentFingerprint\s*=[^=]" src/ --include=*.ts sed -n '85,115p' src/editors/vacuum-maps-section.ts src/vacuum-calibration-write.ts sed -n '460,485p' src/houseplan-onboarding-runtime.ts sed -n '8680,8700p;9560,9600p;9720,9760p' src/houseplan-editor-runtime.ts grep -n "_adoptInitialSpace\|_resumePendingNavMode\|_restoreZoom" src/houseplan-editor-runtime.ts src/houseplan-onboarding-runtime.ts ``` Гейты кода не запускались — этап ревью ТЗ, продуктовый код не меняется этим коммитом. Проверка гейтов относится к код-ревью (§2.7). ## Закрытие раунда r1 | Находка r1 | Чем закрыта в r2 | Где это видно | |---|---|---| | **High-1** — 4 названных вызывающих `_adoptStructuralResponses` против 7 реальных; дубликат `space/delete` в `houseplan-editor-runtime.ts:8693` с тем же дефектом M1 #490 не учтён AC2/AC4 | §3 п.2–3 переписаны: семь вызывающих в двух профилях (`reload` ×3, `post-write` ×4, включая оба `space/delete`); §6.3 вводит явный параметр `profile`; §6.4 добавляет host-интерфейс editor-runtime (`:842`); AC2 — «имя не встречается в `src/**` вне модуля» (было: «не существует как метод хоста», не видело второй `_adoptStructuralResponses` в `houseplan-editor-runtime.ts:842` как объявление интерфейса); AC4 расширен на все четыре post-write пути и оба входа `delete`, новый смок `demo/smoke_post_write_adoption.mjs` | Диф §3 п.2–3, §6.3, §6.4, AC2/AC4 в `docs/specs/500-config-adoption-boundary.md`. Построчно перепроверено чтением: все 7 вызовов (`houseplan-card.ts:4350,4527`; `summary-panel-runtime-loaded.ts:625`; `houseplan-onboarding-runtime.ts:476`; `houseplan-editor-runtime.ts:8693,9577,9736`) подтверждены на материале ревью | | **High-2** — 16/6 заявлено, фактически 18 присваиваний `_serverCfg =` в 8 модулях; `vacuum-calibration-write.ts:108` и `editors/vacuum-maps-section.ts:107` не решены AC1 | §3 п.1 — «18 присваиваний в 8 модулях», оба vacuum-файла названы с номером строки; §6.4/AC1 — оба файла явно в объёме переноса на `beginOptimistic`/`stageLocalConfig`/`rollbackOptimistic` модуля, не в allowlist (AC1 требует ноль) | Диф §3 п.1, §6.4, §4 п.5, AC1. Построчно перепроверено: `grep -rn "_serverCfg\s*=[^=]" src/` даёт ровно 18 строк в 8 файлах, включая обе названные; оба vacuum-writer'а вызывают `optimisticAttempt`/`rollbackOptimistic` из `serialized-write-queue.ts`, что подтверждает применимость переноса | Обе High-находки r1 закрыты содержательно, не декларативно: числа и строки кода, названные в r2, совпадают с кодом на материале ревью (см. «Что проверено и корректно» ниже — с деталями по каждому пересчитанному числу). ## Находки ### High-3 — новый текст r2 неверно утверждает, что `_adoptInitialSpace` сегодня не вызывается ни на одном post-write пути; фактически он вызывается в `import/apply` для фиксированного этажа, и именно там AC3/AC4/риск-таблица опираются на обратное **Резюме.** Понятие «два профиля» и связанные с ним утверждения — целиком новый текст r2, ранее не рецензировался. §6.3 (`docs/specs/500-config-adoption-boundary.md:213-216`) формулирует это как факт о текущем коде: > Пост-шаги профиля `post-write`: … `_adoptInitialSpace`/`_resumePendingNavMode`/ > `_restoreZoom` в этот профиль **не входят**: сегодня их там нет, а выбор > пространства после удаления делает `_commitSpace` вызывающего. Список «Особенности вызывающих» (`:226-227`) для `import/apply` называет только `_dirtyPos`/`_sentPos`/`_defPos`, снапшоты устройств, `_signer.invalidate`+`_resign`, `_cfgEpoch++` — без единого слова о выборе пространства. Чтение `src/houseplan-editor-runtime.ts` на материале ревью (обработчик `import/apply`, окружает вызов `_adoptStructuralResponses` на строке 9736) показывает обратное: ``` 9751: const spaces = this.host._serverCfg?.spaces || []; 9752: const nextSpace = result.kind === 'space' && result.space_id 9753: ? result.space_id 9754: : spaces.some((space) => space.id === previousSpace) 9755: ? previousSpace : spaces[0]?.id || this.host._space; 9756: if (this.host._hasFixedFloor) this.host._adoptInitialSpace(this.host._model, true); 9757: else this.host._commitSpace(nextSpace); ``` `_adoptInitialSpace` (объявлен `houseplan-card.ts:3708`, делегат в host-интерфейсе editor-runtime `:841`) — та же функция, что вызывают «reload»-пути `_loadFromServer`/`_reloadConfigOnly` (`houseplan-card.ts:4352,4529`). Она **не** сохраняет `previousSpace`: `_initialSpaceSelection` внутри неё выбирает пространство по собственным правилам (hash/saved/default) и вызывает `_commitSpace(selection.id, true)` независимо от вычисленного тут же `nextSpace`. То есть сегодня после `import/apply` в конфигурации с зафиксированным этажом видимое пространство определяет `_adoptInitialSpace`, а не «попытка сохранить `previousSpace`», которую код вычисляет, но отбрасывает в этой ветке. **Почему это блокирует.** Три места ТЗ прямо опираются на утверждение «`_adoptInitialSpace` не вызывается ни на одном post-write пути сегодня»: 1. §6.3 — сам факт, процитирован выше; 2. AC3 (`:283`) — «профиль `post-write` не вызывает `_adoptInitialSpace`/`_resumePendingNavMode`/`_restoreZoom`» как часть доказательства поведенческой нейтральности; 3. §12, строка риска (`:320`) — «Профиль `post-write` случайно получит шаги `reload`… смок AC4 проверяет, что видимое пространство после Import не меняется» — это неверно для конфигурации с зафиксированным этажом: там пространство **должно** активно выбираться `_adoptInitialSpace`, а не оставаться прежним. Если реализовать буквально по тексту — разработчик, доверяя §6.3, вправе не перенести вызов `_adoptInitialSpace` в обработчик `import/apply` внутри модуля (ведь профиль его «не вызывает» по документу), и после рефакторинга импорт в план с зафиксированным этажом перестанет корректно выбирать пространство — новая регрессия ровно того класса, который issue #500 и задуман закрыть (новый/переписанный путь пропускает правило adoption). Если же разработчик решит буквально удовлетворить AC4/смок «пространство после Import не меняется» — это будет расходиться с сегодняшним поведением фикс-этажных планов, то есть сам смок и есть некорректный свидетель поведенческой нейтральности для этого случая. **Что не задето.** `_resumePendingNavMode` и `_restoreZoom` на post-write путях действительно не встречаются (проверено тем же grep — оба совпадения в `houseplan-editor-runtime.ts:2443,7534` относятся к geometry-undo и inbox-навигации устройства, не к четырём post-write путям). Часть утверждения §6.3 верна; неверна ровно доля про `_adoptInitialSpace`. **Что нужно.** Скорректировать §6.3 (снять «сегодня их там нет» в части `_adoptInitialSpace`), явно дописать в «Особенности вызывающих» для `import/apply` ветку `_hasFixedFloor` (`_adoptInitialSpace` вместо `_commitSpace(nextSpace)`) как caller-specific поведение, которое переносится 1:1 и не входит в общие пять шагов профиля `post-write`; исправить формулировку AC3 и риск-строку §12, чтобы смок AC4 проверял неизменность пространства только для не-фикс-этажного случая, а для фикс-этажного — что `_adoptInitialSpace` по-прежнему вызывается после adoption (не теряется). ## Что проверено и корректно (сверх унаследованного) Все точечные числа, добавленные или изменённые в r2, перепроверены построчным чтением и совпадают с кодом на материале ревью: - **Семь вызывающих `_adoptStructuralResponses`** — подтверждено: 3 reload (`houseplan-card.ts:4350,4527`; `summary-panel-runtime-loaded.ts:625`) + 4 post-write (`houseplan-onboarding-runtime.ts:476`; `houseplan-editor-runtime.ts:8693,9577,9736`), плюс объявление метода (`houseplan-card.ts:4231`) и два интерфейсных объявления (`summary-panel-host.ts:87`, `houseplan-editor-runtime.ts:842`) — ровно как в §3 п.2 и AC2. - **Оба `space/delete` — байт-в-байт близкий дубликат с тем же дефектом**: построчное сравнение `houseplan-onboarding-runtime.ts:466-478` и `houseplan-editor-runtime.ts:8683-8695` подтверждает идентичную структуру и идентичную перезапись `_cfgRev`/`_layoutRev` из ответа `delete` после adoption в обеих копиях — ровно так, как описывает §3 п.3. - **`_serverCfg =` — 18 присваиваний в 8 модулях.** Полный пересчёт (с исключением ложных `===`/`!==` срабатываний, которые давал более грубый паттерн) даёт: `houseplan-card.ts` 6, `houseplan-editor-runtime.ts` 6, `plan-optimize-write.ts` 1, `serialized-write-queue.ts` 1, `space-copy-runtime.ts` 1, `summary-panel-runtime-loaded.ts` 1, `vacuum-calibration-write.ts` 1, `editors/vacuum-maps-section.ts` 1 — сумма 18, модулей 8. Совпадает с §3 п.1 дословно, включая номера строк обоих vacuum-файлов (107/108). Побочное наблюдение: r1 в скобках утверждал, что `houseplan-editor-runtime.ts` «фактически даёт 8, не 6» — при пересчёте это не подтвердилось: грубый `grep` без фильтра `!==`/`===` ловит сравнения `this.host._serverCfg === cfg` (`:7521,8243`) как ложные совпадения; после фильтра реальных присваиваний — ровно 6, как и было в исходном ТЗ и как осталось в r2. Не находка (число в r2 верно), но стоит явно зафиксировать: сам аргумент r1 в этой частности был артефактом паттерна, а не второй скрытой находкой, оставшейся неисправленной. - **`_layoutRev =` — 7 присваиваний в 4 модулях** (`houseplan-card.ts` 4, `houseplan-onboarding-runtime.ts` 1, `houseplan-editor-runtime.ts` 1, `plan-optimize-write.ts` 1, после исключения объявления поля) — совпадает с §3 п.1. - **fingerprint config — 10 мест в 5 модулях, fingerprint layout — 5 мест** — оба числа пересчитаны (после исключения объявлений полей) и совпадают с §3 п.1 дословно. - **Оба vacuum-writer'а действительно используют `optimisticAttempt`/ `rollbackOptimistic`** из `serialized-write-queue.ts` и пишут `host._serverCfg =` напрямую в обход какого-либо централизованного шага — подтверждено чтением тел функций (`vacuum-maps-section.ts:89-113`, `vacuum-calibration-write.ts:85-113`); включение их в перенос на `beginOptimistic`/`stageLocalConfig` (§6.4/AC1) методологически корректно. - Внутренняя согласованность новых разделов: `reason` в сигнатуре `adoptAuthoritativeGated` расширен на `'optimize-undo'`/`'import-apply'` синхронно с §3/§6.3; мутант `post-write-skips-asset-gate` (§11) корректно заменяет прежний `space-delete-skips-asset-gate`, покрывая все четыре пути, а не один. - §15 п.7 — новый пункт «два профиля, а не один» — обоснование не голословно: реальный набор пост-шагов у `reload` и `post-write` сегодня действительно разный (подтверждено тем же чтением, что и выше), склейка профилей была бы поведенческим изменением, а не только рефакторингом. ## Унаследовано из r1 Следующее принято без повторной проверки в r2, дельта их не касается: - Обязательные разделы §7.1 присутствуют все — проверено в `docs/reviews/SPEC-REVIEW-500-r1.md`, материал `ca070673`. Раздел «Статус ТЗ» и структура файла в r2 не менялись содержательно (только пометка «r2 — учтены High-1/High-2»). - Отказ от `small` обоснован названными критериями §5 (сложность/риск 7/10, 7 модулей, state-контракт) — §4 ТЗ не менялся в r2, вывод r1 остаётся в силе. - Продуктовых вопросов владельцу нет, блок §15 «принято предположительно» использован по назначению — методология не изменилась, новый пункт §15.7 ей соответствует (проверен отдельно выше). - `houseplan-card.ts` — 13699 строк при бюджете `core-file-budget` 13700 (§3.5/AC7) — число не затронуто дельтой r2, унаследовано из r1. - Восемь методов `SummaryPanelHost` (`summary-panel-host.ts:84-94`), сведение к одному — не затронуто дельтой (§6.4 в этой части не менялся), унаследовано из r1. - Смок `demo/smoke_summary_panel.mjs` содержит сценарий `recoveryPreparesBackdropBeforeAdoption` — не затронуто дельтой, унаследовано из r1. - I2/I4 (ревизия только вместе с телом; ссылочная идентичность без клонирования) корректно отражают текущее поведение — раздел §6.1-6.2 не менялся дельтой r2 по существу, унаследовано из r1. - Скоуп по `docs/SCOPE.md` (J6, техдолг, без новой пользовательской поверхности) и корректность §5 «не входит» (lifecycle registry #493/#425, umbrella-неизменяемость #34/#425) — не затронуты дельтой, унаследованы из r1. - AC5 (тёплый старт), AC6 (optimistic rollback), AC7 (бюджеты), AC8 — дельта r2 их не редактировала (кроме синхронизации со счётчиками §3, что проверено выше отдельно) — содержательно унаследованы из r1, где они уже входили в «что проверено и корректно». Материал для обеих ссылок — `docs/reviews/SPEC-REVIEW-500-r1.md`, SHA `ca07067386dee0ce2bfad64c169404c9284cd242` (дерево `183f1ddc6b6261a2345fcc84502883e6274a9b70`). ## Чего не проверял - Достижимость самого рефакторинга (перенос кода) — кода ещё нет, ревью ТЗ оценивает план. - Автотесты/typecheck/build не запускались — класс изменения r2 докс-онли, как и r1; гейты кода — предмет код-ревью (§2.7). - Полный построчный аудит `plan-optimize-write.ts`/`space-copy-runtime.ts` на предмет иных, не названных ни в r1, ни в r2 расхождений — не проводился; фокус остался на утверждениях, которые дельта r2 реально вводит или пересчитывает (профили, семь вызывающих, писатели identity), а не на повторном полном аудите всех шести исходно названных модулей. - Не проверялось, есть ли у `optimize_undo`/`import/apply` собственные расхождения того же типа, что найден у `import/apply` (`_adoptInitialSpace`), сверх этого одного — например, полная построчная сверка `_cfgEpoch++`/ `_signer.invalidate`/`_resign` списка на предмет пропущенных вызовов; проверена только заявленная в находке High-3 область (выбор пространства). Если High-3 будет чиниться правкой этого блока, стоит перечитать соседние строки той же функции ещё раз в r3. ## Вердикт High: 1 (High-3, в скоупе задачи — чинится автором тем же текстовым изменением ТЗ, отдельный issue не заводится). Medium: 0. Обе High-находки r1 закрыты содержательно и проверяемо. Но дельта r2 ввела новое понятие («два профиля»), и именно в нём — в описании того, что `import/apply` делает сегодня — обнаружена новая, ранее не рецензированная фактическая неточность того же класса, что High-1/High-2: уверенное утверждение о коде («сегодня их там нет»), которое не подтверждается чтением, и на которое опирается конкретный AC (AC3) и конкретный смок (AC4). Задача не может уйти в разработку, пока инвентаризация `import/apply` не учитывает `_adoptInitialSpace` в ветке `_hasFixedFloor` — иначе рефакторинг рискует тихо уронить выбор пространства после импорта в конфигурациях с зафиксированным этажом, то есть повторить дефект класса M1 #490 внутри задачи, которая создана его закрыть. **Вердикт: жёлтый · заход r2 · блокирующих циклов 2/4 · High: 1 · Medium: 0 → в задаче** --- --- ## Материал раунда - Ветка: `issue/500-config-adoption-boundary`, коммит `c35ecf13206b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `9a354a9fa366fcca195773d22a15f7947caf5d14` ``` git log --all --format='%H %T' | grep 9a354a9fa366 ``` - ТЗ `docs/specs/500-config-adoption-boundary.md`, блоб `5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9` ``` git log --all --find-object=5cfc36e2614361d1e90d4d9a8a9a7358d4bec3b9 -- docs/specs/500-config-adoption-boundary.md ``` - Вердикт конвейера: `yellow` · High 1