# CODE-REVIEW-272-r1 - Issue: [#272](https://github.com/Matysh/houseplan-card/issues/272) — Multi-wall стыки beta.5 всё ещё оставляют белые треугольные отверстия - Ветка: `issue/272-no-multiwall-holes` - HEAD ревью: `5fd4e2249a4888f9098efff52e975b6a363b4747` (после ребейза на `dev`, интеграция #271 подтверждена автором отдельным комментарием) - Заход: r1 (первый код-ревью цикл для #272) · блокирующих циклов израсходовано 0/4 - Спецификация: `docs/specs/272-no-multiwall-holes.md`, спек-ревью r1 жёлтый → r2 зелёный (обе Medium-находки закрыты текстом ТЗ до начала кода) ## Скоуп проверки Диапазон: `git diff origin/dev...HEAD` (merge-base `67863d6`). Изменённые файлы: - продукт: `src/wall-thickness.ts`; - тесты/evidence: `test/wall-thickness.test.mjs`, `test/golden-matrix.test.mjs`, `demo/smoke_multiwall_junction.mjs`, `demo/golden/harness.mjs`, `demo/golden/matrix.mjs`, `scripts/mutation-gate.mjs`; - документация: `docs/ARCHITECTURE.md`, `docs/WALL-THICKNESS.md`, `docs/TESTING.md`, `docs/USER-GUIDE(.ru).md`, `docs/CHANGELOG(.ru).md`, `docs/images/*`, `docs/images/screenshots.json`, `docs/specs/272-*.md`, `docs/reviews/SPEC-REVIEW-272-r{1,2}.md`; - сгенерированное: `dist/houseplan-card.js`, `custom_components/houseplan/frontend/houseplan-card.js` (класс D, в паре с исходником). Ни один файл модели/сохранённых ссылок (rooms/walls/layout/marker.space/open_spans) не тронут — правка целиком в рендер-геометрии `wall-thickness.ts`, что совпадает с заявленным «не входит» ТЗ §5. Трейлеры: коммит `3fedcaf` (продукт+тесты+документация) несёт `Issue: #272` / `User-Visible: yes` и правит оба changelog в одном коммите. Коммит `5fd4e22` (обновление скриншотов после Linux-прогона) — `User-Visible: no`, корректно (не новое поведение, а пересъёмка фингерпринта). ## Что чинит правка Корень дефекта (подтверждён чтением): `multiWallBevelTrianglesAt()` (переименована в `multiWallBevelCutsAt()`) строит для каждой пары соседних лучей узла треугольник `[qA, qB, hit]`, где `hit` — пересечение offset-линий. Когда `hit` лежит дальше `R`, треугольник вычитается из тела/paper. Проблема: стороны `qA-hit` и `qB-hit` сходятся в `hit` ровно в одной математической точке — с точки зрения polygon-clipping (`polybooljs`) и SVG-заливки это не топологическая связность, и вырезанный треугольник остаётся отдельным замкнутым кольцом (дырой), а не частью внешнего фона. Правка добавляет к каждому такому cut маленький квадратный «коннектор», отцентрованный на `hit`, идущий вдоль луча узел→hit и поперёк него на `bridge = min(max(8ε, 0.05·halfDepth), 0.25·clearance)`, где `clearance = distance − R`. Он одновременно перекрывает часть уже вырезанного треугольника (в сторону узла) и уже пустого внешнего сектора (в сторону от узла), физически соединяя их в одну связную пустоту. Коннектор применяется только при `connectToExterior=true` — в локальной реконструкции узла (`bevelMultiWallBody`) и в `bevelMultiWallPaper`; экспортируемая `multiWallBevelTriangles()` (без коннектора) используется тестами #249/#261 для проверки формы самого клина в пределах `R`, поведение которой не изменилось. ## Как проверялось (гейты) | Гейт | Команда | Результат | |---|---|---| | Typecheck | `npx tsc --noEmit` | green | | Unit | `npm test` | 1166 passed / 0 failed | | Build + bundle parity | `npm run build`; `cmp dist/... custom_components/.../houseplan-card.js`; `npm run bundle:sync` | green, три копии бандла идентичны, `git status` чист | | Docs fingerprint | `node scripts/check-docs.mjs` | green (7 файлов, 10 внешних ссылок) | | Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 2 «зарегистрированные связи»: `smoke_multiwall_junction.mjs`, `smoke_junction_patch_resilience.mjs` (обе через символ `multiWallBevelTriangles`) | | Целевой смок (изменён) | `node demo/smoke_multiwall_junction.mjs` | green, `*HasNoEnclosedHoles` = true во всех 21 полях (Plan/paper/light/View/kiosk/Static/hidden Iso), кеш-инварианты (`stateTickReuses*`) не нарушены | | Целевой смок (регресс #197/#249/#261/#271) | `node demo/smoke_junction_patch_resilience.mjs` | green, все 44 поля true | | Мутант (новый, AC6) | `node scripts/mutation-gate.mjs --id=multi-wall-exterior-corridor-disabled` | поймано 1/1 — тест `issue #249 bounds...` краснеет без коннектора | | Мутант (переименованный, регресс #261) | `node scripts/mutation-gate.mjs --id=multi-wall-paper-full-origin-cut` | поймано 1/1 | | Golden (целевой, AC5) | `node demo/golden/run.mjs --mode=capture --scenario=multiwall-junction-bevel-view-dark` | `different` (растр ожидаемо иной, baseline не принимался) — семантический гейт (`enclosedHoles: 0`, discardedWedgeProbe пуст) не бросил исключение, то есть прошёл | Не прогонялось, и почему: - **Полный `npm run golden:verify`** — запрещён самим скриптом для точечного сценария (`golden verify must run the complete matrix`); это предрелизный гейт (PROCESS.md §8), а не гейт код-ревью. Целевой `--mode=capture --scenario=...` выше даёт эквивалентную семантическую проверку. - **Полный набор `demo/smoke_*.mjs`** — diff не задевает ничего вне multi-wall bevel геометрии; `smoke-select.mjs` вернул только две связи, обе прогнаны. - **`npm run invariants -- --config <...>`** — не запускался: diff не трогает сохранённые edges/thickness-записи/`layout`/`marker.space`/`open_spans`, а только приватную рендер-геометрию `wall-thickness.ts` (треугольники вычитаются из вычисляемого тела на лету, не из модели). Триггер геометрии/ ссылок из PROCESS.md §8 сюда не относится. - **`python -m pytest tests_backend`** — ни один `.py`-файл не менялся. - **Performance-профили** — не названы в AC, не затронуты (кеш HA/theme-тиков подтверждён смоком, `O(E²)` контракт из §7 ТЗ не тронут: цикл всё так же по парам лучей одного узла). - **Linux Docs screenshots workflow** — не перезапускался мной; автор уже прогнал его на точном SHA (`.../actions/runs/32658074504`, приложена ссылка в issue) и объяснил три раздельно проверенных малых raster-дельты (15/15/21 px). Локальный `check-docs.mjs` подтверждает, что текущий фингерпринт соответствует `src/**`. ## AC — построчно - **AC1** (нет запертых holes для реальных классов node). Доказано частично синтетикой (`test/wall-thickness.test.mjs`, тест «issue #249 node classification…», T и X, равные/смешанные толщины, оба `cell_cm: 5` и `1` — добавленный `fineGridFan` блок) и частично переиспользуемой анонимной реальной 3-лучевой fixture `test/fixtures/249-multiwall-junction.json` (`cell_cm: 30`, неортогональные углы — настоящая, хоть и старая, production-топология). `assertNoEnclosedLocalHoles` проверяет `roomGeom`, final `geom` и `paperGeom` для каждого случая. **Зафиксировано как наблюдение ниже**, не блокирует. - **AC2** (#249 остаётся ограниченным). Экспортируемая `multiWallBevelTriangles()` без коннектора не изменилась по геометрии; существующие assert'ы на `discardedWedgeProbe` и границу `R` по-прежнему проходят (подтверждено прогоном смока: `excessWedgeIsEmpty: true`). `MITRE_LIMIT`/ `MULTI_WALL_JOIN_LIMIT` не тронуты (проверено `grep` диффа). **Доказано.** - **AC3** (#261/#271 не регрессируют). `smoke_junction_patch_resilience.mjs` зелёный целиком (measured wedge, finite-ray endpoints, lateral probe — все true). Единственное изменённое число — `geometryArea(geometry.geom)` в тесте #197 (124244.27 → 124242.79, ~0.0012% модели), с явным комментарием `// #272 additionally opens any point-contact bevel cut to the exterior`; величина ожидаемо и мизерно уменьшилась из-за новых коннекторов на нескольких узлах фикстуры. **Доказано.** - **AC4** (browser surfaces). `smoke_multiwall_junction.mjs` использует настоящий `SVGGeometryElement.isPointInFill()` с локальным flood-fill для Plan/paper/View/kiosk/Static (растровые SVG-поверхности) и векторный ring-inventory для hidden Iso и light/sun barriers (чистые JS-геометрии, у которых нет SVG). Прогон зелёный. **Доказано, тест умеет падать** — подтверждено мутантом ниже. - **AC5** (golden не может принять клин). `matrix.mjs` объявляет `enclosedHoles: 0` вместо временного `2` из #270; `harness.mjs` делает локальный flood-fill до pixel-сравнения и бросает исключение при несовпадении инвентаря независимо от доли кадра; `test/golden-matrix.test.mjs` добавляет проверку, что каждая multi-wall-сцена объявляет целое неотрицательное `enclosedHoles`. Целевой `golden:capture --scenario=...` подтвердил: семантика прошла, растр `different` (не принят). **Доказано.** - **AC6** (мутант ловит слепой класс). Новый мутант `multi-wall-exterior-corridor-disabled` возвращает cut к точечному контакту (`multiWallBevelCutsAt(..., true, false)` вместо `true, true`) и обязан покрасить тест `issue #249 bounds...` — подтверждено прогоном (1/1 поймано). Существующий `multi-wall-paper-full-origin-cut` тоже переименован под новую сигнатуру и по-прежнему ловит регресс paper (1/1). **Доказано.** - **AC7** (privacy/determinism). Ни один файл с полным пользовательским экспортом не закоммичен (`git diff --stat` не показывает новых fixture-файлов вообще). Permutation/reversed/scale-инварианты для fan-теста подтверждены (`closeTo(geometryDifferenceArea(...), 0, 1e-6)` между base- и fine-grid-геометрией и между permuted/base). **Доказано.** - **AC8** (локальные гейты). См. таблицу выше — все обязательные зелёные. ## Находки Блокирующих (High) находок нет. Medium в скоупе или вне скоупа — нет. **Low 1 (снято без правки).** AC1 в ТЗ формулирует требование как «minimized fixtures из **обоих экспортов**» (1.json `cell_cm: 5`/12 узлов, 2.json `cell_cm: 1`/14 узлов). Правка не добавляет новый fixture-файл, извлечённый из этих двух реальных экспортов: она переиспользует старую fixture #249 (`cell_cm: 30`, из более раннего issue) плюс синтетические углы/толщины, подобранные автором теста напрямую в коде (`cases` в «issue #249 node classification…»). Формально степень соответствия «minimized fixture из экспорта» против «представительный синтетический тест» неотличима снаружи — приватные экспорты не в репозитории, и я не могу сверить конкретные числа `cases` с реальными узлами владельца. По существу семантический контракт (нулевые holes) при этом доказан на representative T (равные/смешанные толщины, неортогональные углы — как в реальной #249-fixture) и X (ортогональные равные/смешанные) узлах на обоих `cell_cm: 5` и `cell_cm: 1`, а также независимо подтверждён browser `isPointInFill` и golden-семантикой на реальной production-геометрии сцены `multiwall-junction-bevel-view-dark`. Практического пробела в покрытии не вижу — снимаю без правки, не блокирует. **Low 2 (снято без правки).** Ветка `if (length > map.epsilon && clearance > map.epsilon)` пропускает добавление коннектора, когда узел лишь чуть-чуть превышает `R` (клиренс ≤ epsilon). В этом случае сам вырезаемый треугольник `[qA, qB, hit]` тоже вырожденно мал (обе стороны сходятся почти в одной точке уже на входе), и `stableJunctionPatch` с высокой вероятностью схлопывает его до нуля ещё до попадания в `polybooljs` — отдельного теста на этот пограничный случай нет, но естественный сценарий (реальная толщина стены/зазор ощутимо больше 1e-9×scale) делает его непрактичным. Не блокирует. ## Что проверено и корректно - Топологическая идея коннектора (маленький квадрат, перекрывающий обе стороны точки касания) корректно устраняет именно тот класс дефекта, который описан в issue: «один математический контакт» → «конечная физическая связность», не меняя ни `R`, ни оси лучей, ни существующий discarded-wedge-контракт. - Коннектор применяется только к узлам degree 3+ (`buildMultiWallNodeMap` фильтрует `rays.length < 3` при построении карты, до `bevelMultiWallBody`/ `bevelMultiWallPaper`), обычные двулучевые углы не затронуты. - Кеш-инварианты (`_wallUnionCache`, `_lightBarriers().fingerprint`) не инвалидируются на HA/theme-тик — подтверждено смоком, соответствует §7 ТЗ («без нового state-tick traversal»). - Оба changelog и `USER-GUIDE(.ru).md` обновлены в одном коммите с продуктовым кодом; формулировки описывают видимое поведение («стык читается как единое сплошное тело»), не термины реализации. - `docs/ARCHITECTURE.md`/`docs/WALL-THICKNESS.md`/`docs/TESTING.md` описывают именно то, что делает код (сверено построчно с диффом `src/wall-thickness.ts`). - Рекомендованный в ТЗ порядок мерджа (#271 → #272 → обновление #270) соблюдён; автор явно подтвердил интеграцию отдельным комментарием с полным набором прогонов на объединённом дереве. ## Чего не проверял - Полный `golden:verify`, полный набор `smoke_*.mjs`, performance-профили и Linux HA backend harness — предрелизные гейты, не гейт код-ревью; см. таблицу выше с обоснованием по каждому. - Не сверял числовые значения синтетических `cases` в «issue #249 node classification…» с фактическими узлами приватных экспортов владельца — они не в репозитории (см. Low 1). - Не проверял вручную в браузере (headless-смоки — да, ручного захода в UI не делал); визуальную идентичность обновлённых `docs/images/*.png` принимаю на основании приложенной автором ссылки на зелёный Linux-прогон Docs screenshots и локально зелёного `check-docs.mjs`. ## Вердикт Зелёный. Ни одной High- или Medium-находки; два Low сняты с записью, без правки кода. AC1–AC8 доказаны автотестом там, где заявлено, либо разобраны чтением с явным допущением (Low 1/2) — реализация решает заявленный сценарий (белые треугольники в multi-wall стыках исчезают у всех рассмотренных consumers) и не деградирует ни один из смежных контрактов (#249 mitre limit, #261 retained wedge, #271 finite rays, #197 fail-isolation, кеш-инварианты рендера).