# CODE-REVIEW-113-r2 — honest optional contract for `_spaceModel()`, post-rebase re-check - **Issue:** https://github.com/Matysh/houseplan-card/issues/113 - **ТЗ:** `docs/specs/113-optional-space-model.md`, ревью ТЗ — `docs/reviews/SPEC-REVIEW-113-r1.md` (зелёный, Medium-1 → #184, не блокирует) - **Предыдущий цикл:** `docs/reviews/CODE-REVIEW-113-r1.md` — зелёный, High 0, Medium 0. Слияние автоматически не прошло (конфликт `issue/113-optional-space-model` ↔ `dev`), поэтому issue вернулся в `S6-in-progress` не за переделку кода, а за ребейз (AGENTS.md «Two-agent workflow»: «после ребейза на сдвинутый `dev` это другой код, и принимать его без проверки нельзя»). - **Диапазон:** `origin/dev..HEAD`, `origin/dev` = `c1676cf` (docs: review document for #117). `git merge-base origin/dev HEAD` = `c1676cf` — ветка полностью перебазирована, дивергенции нет. - **Коммиты (5):** `0c2a5de` (specify contract), `135497b` (spec review doc), `1e8503b` (**реализация** — «Make empty space model explicit», это переехавший `09b5a74` из r1), `047363c` (r1 code-review doc), `66fa8f4` (**новый после r1** — «Keep mutation anchor aligned after optional model change»). - **Цикл:** r2/4 (счётчик по этапу код-ревью; ревью ТЗ отдельно на r1) - **Роль:** ревьюер кода (Claude), свежая сессия, без контекста реализации автора и без контекста собственного прогона r1. ## 1. Скоуп цикла r2 `dev` продвинулся между r1 и этим ребейзом: слиты #132/#157 (hosted/partition openings) и #173 (unified wall topology). Автор перебазировал `issue/113-optional-space-model` на новый `dev` (`c1676cf`) и разрешил конфликты, сохранив обе стороны. Это **другой код**, чем тот, что получил зелёный вердикт в r1 — задача этого цикла не «доверять r1», а заново пройти по чек-листу §2.7 на итоговом дереве. Diff `origin/dev...HEAD` (16 файлов) идентичен по содержанию r1 плюс один новый коммит: - `66fa8f4` меняет **только** `scripts/mutation-gate.mjs`: якорь мутации `same-space-room-change-recenters` использовал буквальный текст `prevPos.s === targetSpace`, но ребейз/реализация переименовала локальную переменную в `targetSpaceId` (проверено чтением `src/houseplan-card.ts:12908,13018,13033` — переменная везде называется `targetSpaceId`, старого имени `targetSpace` в файле нет). Мутационный якорь — точное совпадение строки, поэтому несовпадающий текст сделал бы мутационный тест **тихо неработающим** (find не совпадает → патч не применяется → «поймано» покажет ложный `OK`, а не реальную проверку). `66fa8f4` возвращает якорь к соответствию исходнику. - `Issue: #113` / `User-Visible: no` на месте на всех 5 коммитах, включая новый. Остальные 15 файлов — то же содержание, что было предметом r1 (проверено `git diff`: единственный новый функциональный код появляется только в `66fa8f4`). Часть строк diff'а (`_saveMarker`, `partitions`) теперь физически стоит рядом с кодом #132/#157/#173, слитым в `dev` до ребейза — эти call sites `_spaceModel()`/`.partitions` уже были в скоупе оригинальной реализации (видно по `targetSpaceModel`, `space.partitions`/`model.partitions` в diff'е — не новые правки этого цикла, перенеслись без изменений при ребейзе). Отдельно проверено: слитый в `dev` код не добавил **новых** необработанных обращений к `_spaceModel()` вне diff'а этой ветки (см. §3). `User-Visible: no` остаётся в силе без изменений (§13 ТЗ), changelog не трогается — согласовано. ## 2. Как проверялось — таблица гейтов | Гейт | Команда | Результат | |---|---|---| | Typecheck | `npx tsc --noEmit` | OK, без ошибок | | Unit | `npm test` | `877/877` OK (было `811/811` в r1 — рост за счёт тестов, слитых в `dev` вместе с #132/#157/#173, не связан с этим диффом) | | Build + 3 копии бандла | `npm run build`, затем `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` и `cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | OK, обе сверки — байт-в-байт совпадение | | Именованный smoke (AC3–AC6) | `node demo/smoke_optional_space_model.mjs` | OK, все 9 подпроверок `true` | | Именованный smoke (AC6, read-only cold start) | `node demo/smoke_readonly_cold_start.mjs` | OK, все 13 подпроверок `true` | | Мутационный guard AC10/AC4/AC5 (не менялся этим циклом) | `node scripts/mutation-gate.mjs --id=empty-space-cleanup-disabled` | `поймано 1 из 1`, чистый прогон зелёный | | **Мутационный guard, якорь которого правит `66fa8f4`** | `node scripts/mutation-gate.mjs --id=same-space-room-change-recenters` | `поймано 1 из 1` — якорь совпал с исходником, патч применился, `smoke_subarea.mjs` покраснел, как обязан. Это прямое подтверждение, что правка якоря не сделала тест немым | | Именованный smoke затронутого якоря | `node demo/smoke_subarea.mjs` (чистое дерево) | OK, все 14 подпроверок `true` | | Regression: поверхности, слитые из #132/#157/#173, рядом с которыми правился diff (`_saveMarker`/`partitions`) | `node demo/smoke_partition_openings.mjs`, `node demo/smoke_unified_wall_tool.mjs`, `node demo/smoke_room_autoclose.mjs` | OK, все зелёные | | Process gate | `node scripts/process-gate.mjs --issues` | `диапазон origin/dev..HEAD, коммитов 5`, `гейт пройден, предупреждений 0` | | Контракт call sites | `grep -n "_spaceModel()!" src/houseplan-card.ts` и `grep -n "_spaceModel()\s*\." src/houseplan-card.ts \| grep -v "?\."` | пусто — ни non-null assertion, ни naked deref после `_spaceModel()` в production source | **Не прогонялось, и почему:** - **Полный набор 135(+) браузерных смоков** — не расширился в этом цикле относительно r1: этот ребейз не добавил новых поверхностей, кроме тех трёх (`partition_openings`, `unified_wall_tool`, `room_autoclose`), которые были рядом с изменёнными строками и прогнаны выше. Остальное логическое обоснование r1 (guard, недостижимый при непустой модели) не изменилось — новый функциональный код этого цикла ограничен одной правкой строки в тестовом скрипте, не в продукте. - **`npm run golden:verify`** — не прогонялся; тот же аргумент, что и в r1 (guard добавлен *до* существующей логики и физически недостижим при непустой модели), плюс этот цикл не меняет ни одной строки `src/houseplan-card.ts` относительно r1 — только `scripts/mutation-gate.mjs` (класс B, не влияет на визуальный результат). - **`python -m pytest tests_backend`** — ни один файл `custom_components/**/*.py` не тронут ни в этом коммите, ни в диапазоне. - **Performance-профили** — не названы в AC, hot path не тронут этим циклом (единственная правка — тестовый скрипт). - Остальные смоки вне таблицы — сознательно не прогонялись; сужение соразмерно объёму нового кода цикла (одна строка в `mutation-gate.mjs`), а не всей задаче #113 заново. ## 3. Проверка AC (docs/specs/113-optional-space-model.md §10) — переподтверждение после ребейза Продуктовый код (`src/houseplan-card.ts`, `src/space-model-selection.ts`) идентичен принятому в r1 (проверено `git diff` — единственный новый коммит цикла трогает только `scripts/mutation-gate.mjs`), поэтому доказательства AC1–AC9 из r1 переносятся без изменений; ниже — что перепроверено заново на итоговом дереве, а не просто процитировано. | AC | Статус в r2 | |---|---| | AC1–AC2 (optional тип, отсутствие non-null assertions) | перепроверено заново: `tsc --noEmit` зелёный на итоговом дереве; grep на `_spaceModel()!`/naked deref — пусто (см. §2) | | AC3–AC6 (empty lifecycle, delete-last, pending action abort, read-only cold start) | перепроверено исполнением: `smoke_optional_space_model.mjs`, `smoke_readonly_cold_start.mjs` — оба зелёные на итоговом дереве после ребейза | | AC7 (non-empty View/editors pixel/action parity) | перепроверено smoke-регрессией на поверхностях, слитых из #132/#157/#173 и физически соседствующих с diff'ом (`partition_openings`, `unified_wall_tool`, `room_autoclose`, `subarea`) — все зелёные; логический аргумент r1 (guard недостижим при непустой модели) не изменился, т.к. продуктовый код не менялся | | AC8 (stale explicit id не мутирует первый space) | код не менялся; `test/space-model-selection.test.mjs` в составе зелёного `npm test` | | AC9 (active-id fallback) | код не менялся; тот же unit-тест | | AC10 (type/source gates ловят регрессию сигнатуры) | **перепроверено исполнением заново** — `empty-space-cleanup-disabled` (не менялся) и, отдельно, мутационный тест, чей якорь чинит этот цикл (`same-space-room-change-recenters`): подтверждено, что после правки якорь совпадает с исходником и мутация по-прежнему ловится (§2, §4) | ## 4. Дисциплина «тест умеет падать» — что именно проверено исполнением в этом цикле Ядро этого цикла — не переисполнение доказательств r1 (код там не менялся), а верификация того, что единственная новая правка (`66fa8f4`) не превратила существующий мутационный гейт в фиктивный: - До правки старый якорь (`prevPos.s === targetSpace`) не совпал бы ни с одной строкой текущего `src/houseplan-card.ts` (переменная называется `targetSpaceId` во всех трёх местах использования — проверено grep'ом). Несовпадающий `find` в `scripts/mutation-gate.mjs` не бросает ошибку — он просто не применяет патч, тест против чистого дерева проходит с эффективно нулевым мутационным покрытием, а `поймано 1 из 1` в CI осталось бы зелёным без реальной проверки — то есть перед `66fa8f4` этот конкретный guard тихо перестал бы что-либо ловить. - Прогон `node scripts/mutation-gate.mjs --id=same-space-room-change-recenters` на текущем дереве подтверждает обратное: `find` совпал, патч применился, `smoke_subarea.mjs` **покраснел** — тест умеет падать по факту исполнения, не предположительно. - То же самое для `empty-space-cleanup-disabled` (якорь не менялся этим циклом, но перепрогнан заново на итоговом дереве после ребейза, а не процитирован из r1): `поймано 1 из 1`. ## 5. Находки Нет находок High. Нет находок Medium. Два Low из r1 (стейл `_splitSel`/`_mergeDialog`/`_wallDialog` при пустом переходе; недостижимый защитный `if (!space) return nothing` в `render()`) относятся к продуктовому коду, который в этом цикле не менялся — они разобраны и сняты в `docs/reviews/CODE-REVIEW-113-r1.md` §5, повторно не переоткрываю. ## 6. Что проверено и корректно - `git merge-base origin/dev HEAD` совпадает с `origin/dev` — ветка действительно линейно перебазирована, диапазон `origin/dev..HEAD` не содержит посторонних коммитов и не пропускает ни один слитый коммит основной линии. - Продуктовый diff (`src/houseplan-card.ts`, `src/space-model-selection.ts`, `docs/ARCHITECTURE.md`, `docs/TESTING.md`, тесты, smoke) идентичен r1 построчно — единственная дельта цикла ограничена `scripts/mutation-gate.mjs` (класс B). - Call sites, физически расположенные рядом со слитыми из #132/#157/#173 фичами (`_saveMarker` targetSpaceModel/targetSpaceId, `.partitions` в `_openPairs`/соседних хелперах), уже были частью оригинальной классификации #113 (видно по `targetSpaceModel`/`space.partitions` в diff'е — не новые изменения этого цикла); слитый код сам по себе использует новую optional-сигнатуру корректно там, где обращается к `_spaceModel()` (например, `_spaceModel()?.partitions` в коде, пришедшем из `dev`) — проверено чтением и не нарушено ни одним из трёх smoke по этим поверхностям. - `docs/ARCHITECTURE.md`: баннер «Updated» корректно продвинут на дату ребейза, инвариант optional-space описан консистентно с кодом; `docs/TESTING.md` чек-лист ссылается на реальные smoke/unit/mutation имена, все они существуют и зелёные. - Трейлеры на всех 5 коммитах диапазона: `Issue: #113`, `User-Visible: no` — включая новый `66fa8f4`. `node scripts/process-gate.mjs --issues` зелёный, 0 предупреждений. - `tsconfig.test.json` и `docs/specs/README.md` — механические регистрации нового файла/записи, соответствуют содержимому. ## 7. Чего не проверял - Полный браузерный набор смоков и `golden:verify` — обоснование в §2 (нулевой продуктовый diff относительно r1, узкий новый код цикла — одна строка в тестовом скрипте). - `pytest tests_backend` — класс A/B бэкенда не тронут. - Performance-профили — не названы в AC, hot path не тронут ни в r1, ни в этом цикле. - Содержательный повторный аудит AC1–AC9 «с нуля» без опоры на r1 — не требуется: код, который они доказывают, не менялся байт в байт (проверено `git diff` между продуктовым кодом r1 и текущим деревом), поэтому это не «доверие прошлому вердикту», а подтверждённый факт отсутствия изменений в этой части. - Medium-1/#184 (fallback-семантика ТЗ §6) — по-прежнему отдельный issue, не в скоупе этого код-ревью и не в скоупе #113. ## 8. Вердикт Ребейз на `c1676cf` не изменил ни строки продуктового кода #113: единственная новая правка цикла — восстановление мутационного якоря, сломанного переименованием переменной в результате мержа. Правка проверена исполнением на способность ловить регрессию (не голословно), все фаст-гейты и релевантные smoke зелёные на итоговом дереве, AC1–AC10 подтверждены заново (без изменений там, где код не менялся, и напрямую — там, где менялся). High: 0. Medium: 0. **Вердикт: зелёный · цикл r2/4 · High: 0 · Medium: 0 → нет · Документ: docs/reviews/CODE-REVIEW-113-r2.md**