# CODE-REVIEW-228-r2 - Issue: [#228](https://github.com/Matysh/houseplan-card/issues/228) — надёжное рисование стен и операции с готовым контуром - ТЗ: `docs/specs/228-plan-drawing-problems.md` (ревью ТЗ зелёное, r1, #228) - Ветка: `issue/228-plan-drawing-problems` - Коммит реализации r1: `2aaef12` (`fix: make plan drawing fail closed`) - Коммит фикса r2: `172d5d7` (`fix: close plan repair review findings`, `Issue: #228`, `User-Visible: yes`) - Заход ревью: r2 · блокирующих циклов израсходовано 1 из 4 (r1 был жёлтым и списал единицу; настоящий заход разбирает дельту по §2.10 PROCESS.md / issue #214) - Ревьюер: Claude (код-ревью), сессия без контекста реализации и без контекста r1-ревью ## 0. Предыдущий раунд и SHA Вердикт r1 найден в комментарии issue ([issuecomment-5379067039](https://github.com/Matysh/houseplan-card/issues/228#issuecomment-5379067039)): жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 2 (M1, M2, обе в скоупе). Полный документ — `docs/reviews/CODE-REVIEW-228-r1.md` (в дереве на этом SHA, коммит `9540627`). **Замечание к процессу, не к коду:** сам текст вердикта в комментарии issue SHA не называет — только «Документ: …». SHA восстановлен из шапки документа («Коммит реализации: `2aaef12`») и из хендоффа автора чуть выше («Реализация: 2aaef12») и хендоффа фикса чуть ниже («предыдущий проверенный SHA 2aaef12 → фикс 172d5d7»), так что дельта для этого раунда однозначна: `git diff 2aaef12..172d5d7` (= `git diff 2aaef12..HEAD`, `HEAD` этой ветки — `172d5d7`). Это не блокирует разбор, но стоит перенести в шаблон вердикта: именование SHA явно, а не только через ссылку на документ. Ребейза не было: `git merge-base origin/dev HEAD` не изменился относительно диапазона задачи, `2aaef12` не достижим из `origin/dev` (задача просто ещё не слита) — код, проверенный в r1, и код на этом SHA продолжают одну и ту же историю без переписывания. Дельта локальна, полный разбор не требуется по §2.10/§7.2 AGENTS.md. ## 1. Скоуп проверки — дельта r1→r2 `git diff --stat 2aaef12..172d5d7`: 10 файлов, +420/−24. Продуктовый код — только `src/houseplan-card.ts` (+17/−9, две точки: `_offerExistingWallFace` и `_validateWallRepair`) и новый экспорт в `src/wall-face-repair.ts` (+15, чистая функция `repairMovesHostedPartition`). Тесты — `test/wall-face-repair.test.mjs` (+22/−2, новый unit-тест) и `demo/smoke_plan_drawing_repairs.mjs` (+29, два новых сценария). Остальное — три синхронные копии бандла, оба changelog (RU+EN, `User-Visible: yes` в том же коммите) и сам `docs/reviews/CODE-REVIEW-228-r1.md`, добавленный в дерево отдельным коммитом `9540627` между r1 и фиксом. Дельта задевает ровно AC6/AC7/AC8 (repair ≤2 см, негативная матрица, room dialog) и §8.6 ТЗ — те же строки, что и обе находки M1/M2 из r1. Ни одна другая поверхность (snap-overlay, strict-Shift, delete-room dialog, room-deletion transaction) не тронута — их AC (AC1–AC5, AC9–AC14) наследуются из r1 без повторной проверки, раздел 4. ## 2. Как проверялось — гейты Дешёвые гейты прогнаны полностью (стоят минуты, код изменился): | Гейт | Команда | Результат | |---|---|---| | Typecheck | `npx tsc --noEmit` | зелёный, без вывода | | Unit | `npm test` | `# tests 1032 / # pass 1032 / # fail 0 / # skipped 0` (на 1 больше, чем в r1 — ровно новый тест `repairMovesHostedPartition`) | | Build | `npm run build` | зелёный, `dist/houseplan-card.js` собран за ~12s | | Синхронность бандла | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` и `cmp … demo/srv/assets/houseplan-card.js` | обе копии побайтно идентичны, `git status` после сборки чист | | Named smoke (дельта) | `node demo/smoke_plan_drawing_repairs.mjs` | 16/16 `true`, `OK` | Дополнительно, специально для дисциплины «тест умеет падать» (§8 PROCESS.md, это условие честности сокращения гейтов на дельте): временно откатил `src/houseplan-card.ts` и `src/wall-face-repair.ts` к состоянию `2aaef12`, пересобрал, скопировал бандл в `demo/srv/assets/` и повторно прогнал тот же smoke — рабочее дерево затем восстановлено к `HEAD` и пересобрано, `git status` чист. - на коде до фикса `ambiguousLargeGapUsesWallsFlow` → **`false`** (упал), все остальные 15 проверок остались `true`. Это прямое доказательство того, что проверка M2 фальсифицируема и действительно закрывает найденный дефект, а не переформулирует его условие. - `hostedOpeningBlocksRepair` на коде до фикса остался `true`: ожидаемо — сам инлайновый guard (`if (proposal.sourceKey.startsWith('static:partition|')) …`) в `2aaef12` был по чтению кода уже корректен, M1 была находкой про **отсутствие доказательства**, а не про баг в логике (это прямо сформулировано в M1 из r1: «Guard … по чтению кода верен … Но ни один тест не воспроизводит этот случай»). Для этой находки доказательство падения даёт прямой unit-тест на извлечённую функцию: `repairMovesHostedPartition` в `2aaef12` не существовала вовсе (`grep` по старому файлу — 0 совпадений), поэтому импорт этой функции в `test/wall-face-repair.test.mjs` на коде до фикса завершился бы ошибкой сборки/импорта, а не тихим прохождением. Тест падать умеет, просто не через тот же smoke-сценарий, а через прямой unit. **Чего не прогонял и почему:** - `smoke_plan_snap_overlay.mjs`, `smoke_unified_wall_tool.mjs` — прогнаны в r1 зелёными (34/34, 19/19), дельта r1→r2 не касается ни snap-overlay, ни strict-Shift кода; наследуются без повторного прогона (раздел 4). - `npm run golden:verify`, полный smoke-набор (127 файлов), `pytest tests_backend`, performance-профиль — как и в r1: diff не меняет ни один существующий видимый кадр (правки чинят внутреннюю ветку без нового рендера), ни один `.py`-файл не тронут, задача не задевает все поверхности. Полные наборы — предрелизный гейт (§8 PROCESS.md), не гейт ревью; причины не изменились с r1 и заново не переоцениваю их для дельты, которая эти поверхности не трогает. ## 3. Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | **M1** — AC7 требует `unit + smoke` для «invalid hosted opening», guard в `_validateWallRepair` (houseplan-card.ts:12673, на SHA `2aaef12`) существовал, но не был воспроизведён ни одним тестом | Guard вынесен в именованную экспортируемую чистую функцию `repairMovesHostedPartition` (`src/wall-face-repair.ts:29-40`), покрытую прямым unit-тестом с тремя ветвями (совпадающий host, несовпадающий host/kind, не-partition `sourceKey`); плюс сквозной smoke-сценарий `hostedOpeningBlocksRepair`, который проводит `_offerExistingWallFace` → `_saveRoom()` через реальный hosted-opening на дистанции 1,2 см и проверяет, что `_curSpaceCfg` не изменился и показан именно `toast.wall_repair_changed` | `src/wall-face-repair.ts:29-40` (функция); `test/wall-face-repair.test.mjs:53-70` (unit, 3 ветви); `demo/smoke_plan_drawing_repairs.mjs:104-118` (smoke); вызов на Create-пути — `src/houseplan-card.ts:12674-12676` внутри `_validateWallRepair`, дёргается из `_applyWallFaceBatch` (`:12777`) | | **M2** — широкая (>2 см, screen-scale) диагностика в `_offerExistingWallFace` не различала `diagnostic.kind === 'repair'` и `'ambiguous'`; при ambiguous клик перехватывался и подсвечивался произвольный `proposals[0]` вместо обычного Walls flow (§8.6 ТЗ) | Условие сужено с `if (diagnostic.kind !== 'none')` до `if (diagnostic.kind === 'repair')`; при `'ambiguous'` код больше не входит в ветку, падает через к `if (!face …) return false;` — обычный Walls flow, без toast, без подсветки, без перехвата клика | `src/houseplan-card.ts:7684` (изменённое условие); regression-сценарий `ambiguousLargeGapUsesWallsFlow` в `demo/smoke_plan_drawing_repairs.mjs:94-102`, воспроизведён как падающий на коде до фикса (раздел 2 выше) | Обе находки закрыты фактическим кодом и тестом, который умеет падать (M2 — прямым воспроизведением регресса; M1 — прямым unit-тестом на извлечённую функцию), а не заявлением автора. Хендофф автора ([issuecomment-5379081221](https://github.com/Matysh/houseplan-card/issues/228#issuecomment-5379081221)) совпадает с тем, что видно в дельте. ## 4. Унаследовано из r1 Без повторной проверки в этом раунде, полностью на основании `docs/reviews/CODE-REVIEW-228-r1.md` (в дереве на SHA `9540627`, предмет разбора там — SHA `2aaef12`, тот же код в этих AC дельтой r2 не тронут): - **AC1–AC4** (активная ось/узел на текущем отрезке, fail-closed на неоднозначных endpoint, strict-Shift через точное пересечение луча, цвет подписи по точному вектору) — `src/plan-snap-overlay.ts`, `src/logic.ts` (`isExact45Vector`), рендер `.active-axis`/`.active-vertex`. Дельта r1→r2 не трогает ни один из этих файлов. - **AC5/AC8** (комната из готового контура, `Shift+click` bypass, create-existing-face сохраняет партиции) — кроме уточнённой в M2 ветки широкой диагностики, остальная логика `_offerExistingWallFace` и `findWallFaceAtPoint`/`wall-face-graph.ts` не менялась. - **AC9** (диалог удаления через `hp-dialog`, не `confirm()`) — `src/houseplan-card.ts` вне тронутого дельтой диапазона строк, `src/room-deletion.ts` не в diff. - **AC10–AC12** (Keep/Delete walls материализация и rehost, атомарность Undo, fail-before-mutation порядок) — `src/room-deletion.ts` не в diff r2. - **AC13–AC16** (View не регрессирует, touch/gesture guard не тронут, perf-путь не заходит в hover, backend/schema не тронуты) — ни один из названных в r1 файлов (`src/houseplan-card.ts:_svgPointerMove`, `custom_components/**/*.py`) не в diff r2. - **AC17** (гейты/i18n/changelog/документация уровня r1) — новые i18n-ключи в дельте не добавлялись (использован уже существующий `toast.wall_repair_changed`, проверено `grep` — есть в обоих `src/i18n/*.json` и не создан в этом диффе); changelog RU+EN обновлён именно в фикс-коммите `172d5d7` с точными формулировками под M1/M2 (см. раздел 5). Основание для наследования: дельта `2aaef12..172d5d7` не задевает ни один файл и ни одну строку, от которых зависят перечисленные AC — проверено `git diff --stat` (раздел 1) и точечным чтением затронутых строк (раздел 3). Это не «не проверялось», а «проверено в r1 на коде, который здесь не менялся». ## 5. Дополнительно проверено в этом раунде - Оба changelog (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) правлены в том же коммите `172d5d7`, что и код, с `User-Visible: yes` — трейлеры на коммите корректны (`Issue: #228`, `User-Visible: yes`). Текст обеих правок точно называет оба закрытых поведения: «ambiguous wider gaps remain ordinary drawing» (M2) и «a wall carrying a hosted opening is never moved by repair» (M1) — формулировки не обобщают и не расходятся с кодом. - `repairMovesHostedPartition` вызывается из `_validateWallRepair`, которая используется на **обоих** путях подтверждения repair — `_applyWallRepair` (одиночный, `:12685`) и `_applyWallFaceBatch` (через `repairs[0]`, `:12777`) — оба реально достижимы из `_saveRoom`/`_offerExistingWallFace` флоу, guard не обходится ни одним из них. - Оставление предложения (`_offerExistingWallFace`) hosted-репейра без немедленного отказа и отказ только на Create — соответствует ТЗ §8.6 дословно («Room dialog не применяет repair заранее. Create повторно проверяет … hosted openings»), это не новый пробел, а сознательный двухфазный контракт, уже заложенный в ТЗ и подтверждённый в r1. - Тестовый счётчик: r1 зафиксировал `1031 pass`, здесь `1032 pass` — разница ровно на 1 новый unit-тест M1, без скрытых удалений/пропусков (`0 skipped`, расходится с хендоффом автора «1 skipped», как и в r1 — этот разброс не влияет на вердикт, оба прогона зелёные). ## 6. Чего не проверял - Всё, что унаследовано из r1 (раздел 4) — не переисполнялось повторно; список причин не изменился и заново не переоценивался, кроме подтверждения, что соответствующие файлы вне дельты. - `npm run golden:verify`, полный (127 файлов) smoke-набор, `python -m pytest tests_backend`, performance-профиль — не запускал в этом раунде; причины совпадают с r1 (раздел 2) и дельта их не меняет: новых видимых кадров нет, `.py` не тронут, задача не задевает все поверхности. Это предрелизный гейт (§8 PROCESS.md, §11.4 AGENTS.md), не гейт ревью. - Многопользовательский конкурентный сценарий и touch-специфичные жесты для repair-пути — не переисполнялись; вывод r1 («чтением, не исполнением») не затронут дельтой, которая не трогает `_markupClick`-обёртку или multi-client код. ## 7. Вердикт Обе Medium-находки r1 закрыты кодом и тестом, способным падать; никаких новых находок в дельте не обнаружено. High — 0, Medium — 0. Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0 Готово к очереди на пре-релиз (`S8-merged`).