20 KiB
CODE-REVIEW-272-r1
- Issue: #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-лучевой fixturetest/fixtures/249-multiwall-junction.json(cell_cm: 30, неортогональные углы — настоящая, хоть и старая, production-топология).assertNoEnclosedLocalHolesпроверяетroomGeom, finalgeomи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, кеш-инварианты рендера).