# Код-ревью #229 · заход r3 Вердикт: **зелёный** · заход r3 · блокирующих циклов 2/4 · High: 0 · Medium: 0 ## Скоуп разбора Разбор — по дельте, правило §2.9/§2.10. Предыдущий раунд (r2) получен на SHA `ed13b4a` (документ `docs/reviews/CODE-REVIEW-229-r2.md`; SHA в самом тексте вердикта не назван — восстановлен по `git log` и по фразе автора «Коммит `ed13b4a`» в хендоффе, предшествующем вердикту). Правка по r2 — один коммит `fab0c38` («refactor: one function owns the junction geometry of a space»). Дельта: `git diff ed13b4a..HEAD` — 11 файлов, из них содержательные `src/wall-merge.ts` (+33/-0), `src/houseplan-card.ts` (-16 строк локально, вызов геометрии заменён на одну строчку), `src/plan-optimizer.ts` (аналогично -13), `scripts/mutation-gate.mjs` (2 мутанта перенацелены), `test/wall-merge.test.mjs` (+3 юнита). Остальное — `docs/images/screenshots.json` (только `sourceFingerprint`, автоматически из-за изменившегося бандла; `imageSha256` не менялся, кадры те же) и коммит с документом ревью r2 (`fe2fcef`, не код). Дельта локальна: одна находка (Medium-1 r2), один модуль, ни новая подсистема, ни смена контракта поведения не затронуты — `User-Visible: no` в коммите верен, продуктовое поведение не менялось ни на шаг, только внутренняя организация кода и тестов. Полный повторный разбор не требуется; объём проверки — сама правка плюс всё, до чего она дотягивается (оба вызывающих места и мутационное покрытие обоих путей). ## Находка r2 и её закрытие **Medium-1 (r2, в скоупе, возвращена в задачу):** дефект High-1 (двойное масштабирование `roomPolygons`, из-за которого T-стык партиции к середине комнатной стены не находился) был идентичен в двух местах — `_mergeSpacePartitions` (карта) и `optimizePlans` («Оптимизировать планы») — потому что каждый вызывающий строил геометрию примыканий самостоятельно. Регрессионное покрытие защищало только путь оптимизатора; путь карты, который r1 воспроизвёл вторым способом («в браузере через реальные клики»), не имел ни юнита, ни мутанта. | находка | чем закрыта | где это видно | |---|---|---| | Medium-1: однобокое покрытие двойного масштабирования room-полигонов, защищён только `plan-optimizer.ts` | Геометрия примыканий (комнаты, колонны, концы черновиков) вынесена в одну функцию `spaceMergeGeometry(space, { excludeDraftId })`; оба вызывающих теперь однострочный вызов — масштаб можно ошибиться ровно в одном месте | `src/wall-merge.ts:278-295` (новая функция); `src/houseplan-card.ts:6551-6557` и `src/plan-optimizer.ts:503-506` (оба вызывающих — по одной строке, без собственного построения геометрии) | | — регрессия для обоих путей | Мутанты `partition-merge-rescales-rooms` и `chain-merge-sees-own-draft` перенацелены с `plan-optimizer.test.mjs`/смока на общий `test/wall-merge.test.mjs`, патчат теперь сам `spaceMergeGeometry` | `scripts/mutation-gate.mjs:721-741`; юниты `test/wall-merge.test.mjs:214-262` | Закрытие проверено не по заявлению автора, а исполнением (см. «Как проверялось»): оба мутанта, применённые к текущему коду, красят гвард, а не только исходный. ## Как проверялось Материал: `git diff ed13b4a..HEAD` и `git diff origin/dev...HEAD` (полный диапазон issue). Ручного тестирования в цикле нет — далее только то, что исполнено или разобрано по коду с явной пометкой. **Прочитано и разобрано по коду:** - `src/wall-merge.ts:278-295` (`spaceMergeGeometry`) — сопоставлено построчно со старыми телами обоих вызывающих (см. `git diff` выше): семантика идентична, включая то, что `plan-optimizer.ts` не передаёт `excludeDraftId` — как и раньше, там исключения черновика не было и не нужно (оптимизатор не выполняется во время активного рисования). - Проверено отсутствие циклического импорта: `wall-merge.ts` теперь импортирует `roomPoly` из `./logic`; `logic.ts` не импортирует `wall-merge.ts` (grep — 0 совпадений). Компилируется без ошибок (см. ниже) — само по себе уже доказательство при бандлинге через rollup/tsc. **Проверено исполнением:** - `npx tsc --noEmit` — чисто. - `npm test` — 1007/1007, без изменений к количеству от заявленного автором. - `npm run build` + сверка трёх копий бандла (`dist/houseplan-card.js`, `custom_components/houseplan/frontend/houseplan-card.js`, `demo/srv/assets/houseplan-card.js`) — идентичны побайтово (`sha256 9dd1ec1a…`), `git status` после сборки чист — закоммиченный бандл соответствует исходникам. - `node scripts/check-docs.mjs` — passed (7 файлов, 10 внешних ссылок); подтверждает, что изменение `sourceFingerprint` в `screenshots.json` легитимно и не требует новых кадров (`imageSha256` не менялся). - `node scripts/process-gate.mjs` — гейт пройден, предупреждений 0. - `node demo/smoke_wall_chain_merge.mjs` — OK, все 9 проверок, включая `chainMergesIntoTheWallItTouches`, `draftIsGone` (путь §8.6 через реальные клики). - `node demo/smoke_wall_junctions.mjs` — OK, все 12 проверок (соседняя поверхность — T-стыки и геометрия примыканий). - `node demo/smoke_optimize_coordinate_canonicalization.mjs` — OK, все 13 проверок (путь «Оптимизировать планы»). - **Дисциплина «тест умеет падать» — оба перенацеленных мутанта проверены вручную** (штатный харнесс `mutation-gate.mjs` непригоден для юнит-гвардов до #235, независимо от этой задачи): - `partition-merge-rescales-rooms` (патч возвращает лишнее деление на 1000 внутри `spaceMergeGeometry`): наложен на `src/wall-merge.ts`, пересобран `test-build` (`tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs`), `node --test --test-name-pattern="issue 229" test/wall-merge.test.mjs` дал **2 падения** (`space geometry keeps rooms in the coordinates…` и `a junction on a room side survives…`, `1 !== 0`). Патч снят, повторный прогон — 19/19 зелёных. - `chain-merge-sees-own-draft` (патч заменяет исключение черновика на `void exclude`): тем же способом — **1 падение** (`the chain being finished is not its own junction`). Патч снят, 19/19 зелёных. - После снятия обоих патчей полный `npm test` — 1007/1007, `git status` чист (никаких временных правок не осталось). **Не прогонялось и почему:** полный набор из 127 смоков, `golden:verify`, `pytest tests_backend`, performance-профили — дельта этого раунда не задевает рендер, backend или перф-пути; полные наборы — предрелизный гейт (§8), не гейт ревью соразмерного изменения. Задетые поверхности (карта, оптимизатор, T-стыки, слияние цепочки) покрыты смоками выше и юнитами. ## Унаследовано из r1/r2 Без повторной проверки в этом раунде, дельта не касается их доказательной поверхности: - **AC1, AC3-AC7, AC9** — наследованы уже в r2 из документа `docs/reviews/CODE-REVIEW-229-r1.md` (SHA `50a00e4`); дельта r2→r3 не затрагивает пересчёт `host.t`, материализацию `x/y/angle`, допуски, канонизацию направления, транзитивность/порядконезависимость слияния — их код не менялся. - **AC8 (§8.6, слияние ограничено компонентой связности новой цепочки)** — проверено в r1/r2 чтением и смоком; в r3 логика `seedIds` в `mergeCollinearPartitions` не менялась, изменился только источник геометрии примыканий, который `seedIds` не касается. Перепроверено попутно смоком `smoke_wall_chain_merge.mjs` (`chainMergesIntoTheWallItTouches`) в этом раунде — совпадает. - **AC2 (T-стык партиции к середине комнатной стены)** — критерий из документа `docs/reviews/CODE-REVIEW-229-r2.md` (SHA `ed13b4a`) не наследован вслепую: это и есть предмет дельты r3, перепроверено заново в этом раунде (юнит «a junction on a room side survives, through the shared geometry», исполнен, зелёный; мутант, умеет падать — см. выше). ## Что проверено и корректно - `spaceMergeGeometry` — единственный источник геометрии примыканий для обоих путей; масштаб координат общий с партициями (без лишнего деления), исключение активного черновика — опционально и по умолчанию отключено (важно для пути оптимизатора, где активного черновика не бывает). - Новые юниты (`test/wall-merge.test.mjs:214-262`) покрывают три вещи по отдельности: масштаб полигонов (включая `x/y/w/h`-комнату и комнату без геометрии), исключение черновика (обе ветки), сам T-стык — и делают это на уровне модуля, не дублируя мутационное покрытие смоком. - Трейлеры коммита `fab0c38`: `Issue: #229`, `User-Visible: no` — верно, поведение продукта не меняется, правка рефакторит внутреннюю организацию; changelog не требуется и не тронут. - Импорт `roomPoly` из `./logic` в `wall-merge.ts` не создаёт цикла зависимостей. ## Чего не проверял - Полный набор смоков (127), `npm run golden:verify`, `python -m pytest tests_backend`, performance-профили — не задеты дельтой этого раунда (см. «Как проверялось»). - Сценарий «комната через реальные клики» в смоке — автор honestly сообщил, что не взлетел (`_offerWallFaces` не даёт цепочке завершиться штатно) и оставил покрытие на юнитах общего модуля. Не блокирует: AC2 доказан юнитом на уровне `mergeCollinearPartitions`/`spaceMergeGeometry`, что достаточно — геометрия примыканий и механика предложения стен (`_offerWallFaces`) — разные слои, и дефект r1/r2 жил именно в геометрии. Если понадобится клик-путь через реальную комнату — отдельная работа поверх `_offerWallFaces`, не предмет этой задачи. ## Итог Обе High из r1 и единственная Medium из r2 закрыты по существу и перепроверены исполнением. Дефект устранён на уровне причины (общая функция геометрии), а не симптома — оба мутанта краснеют для обоих путей из одного патча. Задача готова к выходу из цикла ревью.