21 KiB
CODE-REVIEW-249-r2
- Issue: #249 — ограниченная геометрия узла из трёх и более стен
- Этап: code (PROCESS.md §2.7)
- Заход: r2 · блокирующих циклов израсходовано 1 из 4 (r1 — красный, потратил 1; этот заход зелёный и бюджет не расходует, #227)
- Коммит на ревью:
26fa9684777b7557d384883f18bb3d097a171caf("fix: preserve bounded multi-wall floor geometry") - База delta-review:
062a98a1d841a5de633392357dcf0d4264ed3620(коммит, получивший CODE-REVIEW-249-r1, красный) - ТЗ:
docs/specs/249-multiwall-junction-bevel.md(SPEC-REVIEW-249-r2, зелёный) - Материал:
git diff 062a98a1d841a5de633392357dcf0d4264ed3620..26fa9684777b7557d384883f18bb3d097a171caf— целевой delta-review по PROCESS.md §2.10, не полный прогон
Скоуп проверки (по дельте)
Автор заявил исправление H1 (floor выходил за пределы здания на несимметричных многолучевых узлах) и M1 (AC2 не покрывал буквальный кейс «15/50/70 см», три взаимно разные толщины). Дельта r1→r2 касается:
src/wall-thickness.ts: переписанbevelMultiWallBody/multiWallBevelTrianglesAt(пофрагментная перестройка каждого узла в ограниченной маске вместо агрегированного вырезания треугольников), новаяclipInnerContourToRoom/largestOuterContour,innerContourForRoomполучил необязательный параметрsharedRoomWallGeometryи теперь строит чистый пол какdifference(room.poly, roomGeom)с защитным клипом на fallback,wallBodiesGeometry/wallBodiesUnionPathвозвращают новое полеroomGeom(кэшируемая канонические кладка комнат до вырезания проёмов и independent bodies).src/houseplan-card.ts: 6 из 8 вызововinnerContourForRoomв путях рендера пробрасываютthis._wallUnionGeometry()?.roomGeomпятым дополнительным аргументом.test/wall-thickness.test.mjs: восстановлен строгий инвариантfloor_union == room_union − canonical_bounded_walls(тест, который H1 требовал вернуть), добавлен матричный кейсhalves: [1.5, 5, 7](три взаимно разные толщины).demo/smoke_multiwall_junction.mjs: пересчитана координатаdiscardedWedge(геометрия узла сместилась из-за новой retain-to-limit логики; проверил формулой в комментарии — не ослабление, а пересчёт).docs/ARCHITECTURE.md,docs/WALL-THICKNESS.md,docs/TESTING.md, обаdocs/CHANGELOG*.md, docs-скриншоты — обновлены по существу в этом же коммите.
Не в дельте и не перепроверялось заново: golden matrix/harness, spec-файл,
общая продуктовая рамка ТЗ, docs/SCOPE.md — см. «Унаследовано из r1».
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
H1 (High). innerContourForRoom отдавал floor-точку вне здания на несимметричных многолучевых узлах (cornerSplitFixture({outerCm:15, dividerCm:100}), extra=1686.24 против допуска 1e-7) |
innerContourForRoom теперь вычисляет чистый пол как difference(room.poly, roomGeom) — канонической, уже ограниченной 1.25×H кладки, с защитным clipInnerContourToRoom на любом fallback-пути; roomGeom кэшируется в wallBodiesGeometry/wallBodiesUnionPath и пробрасывается в 6 из 8 сайтов рендера |
src/wall-thickness.ts:1797-1835 (сама функция), src/houseplan-card.ts (6 обновлённых call site); тест 'corner Split clean floors equal room union minus canonical bounded walls' (test/wall-thickness.test.mjs:1522) — проверил сам: на 062a98a (до фикса, через временный git worktree) этот же тест падает с extra=1686.2432660548943 — ровно то число, что называла находка H1; на 26fa968 тест зелёный |
| M1 (Medium, в скоупе). AC2 не покрывал буквальный кейс «15/50/70 см» — три взаимно разные толщины | В матрицу cases добавлен { angles: [45, 102, 230], halves: [1.5, 5, 7], bevel: true } — три genuinely разные halfDepth, прогоняется через тот же полный цикл проверок, что и остальные кейсы (baseline/permutation/production-scale/multiWallBevelTriangles/makeFanGeometry + wallBodiesGeometry с реальным bevel) |
test/wall-thickness.test.mjs:628 (новая строка массива cases), выполнение — test/wall-thickness.test.mjs:686-731 (тот же for-цикл, без специального исключения для нового кейса) |
Как проверялось
Гейты, соразмерные диапазону дельты (диапазон трогает src/**, требует
check-docs; смоки — по smoke-select.mjs относительно базы r1, не по
полному набору):
| Гейт | Результат | Прогнал |
|---|---|---|
npx tsc --noEmit |
зелёный, без вывода | да |
npm test |
1117 passed / 0 failed / 0 skipped | да |
npm run build |
зелёный | да |
| SHA-256 трёх копий бандла | совпадают: 69b40f0508a6d3372b6d3acd165a643a27d5d0b11fe845fb4c002e13696a5466 (совпадает с заявленным автором) |
да, вручную |
node scripts/check-docs.mjs |
зелёный (7 файлов, 10 внешних ссылок) | да, обязателен — diff трогает src/** |
node --test целевого теста 'corner Split clean floors equal room union minus canonical bounded walls' |
зелёный на 26fa968; красный на 062a98a (extra=1686.2432660548943) — проверено через временный git worktree, удалённый по завершении |
да, дисциплина «тест умеет падать» подтверждена явно |
node demo/smoke_multiwall_junction.mjs |
зелёный, 15/15 | да — прямой AC1/AC4 гейт |
node scripts/smoke-select.mjs --base 062a98a --head HEAD |
5 прямых совпадений: smoke_glow_fail_dark, smoke_junction_patch_resilience, smoke_multiwall_junction, smoke_wall_thickness_transition, smoke_zero_divider_taper (все ← символы wallBodiesGeometry/_wallUnionGeometry) |
да |
node demo/smoke_glow_fail_dark.mjs |
зелёный, 4/4 полей | да |
node demo/smoke_wall_thickness_transition.mjs |
зелёный, 11/11 полей | да |
node demo/smoke_zero_divider_taper.mjs |
зелёный, 13/13 полей | да |
node demo/smoke_junction_patch_resilience.mjs |
зелёный, 15/15 полей | да — переисполнил заново на этом коммите, не унаследовал из r1 |
Не прогонялось (предрелизные гейты по PROCESS.md §8, не гейт ревью):
полный npm run golden:verify/golden:accept, полный набор из 170 смоков,
performance-профили, backend/HA-harness (Python не тронут дельтой). Golden
baseline не принимался.
Что проверено и корректно (по дельте)
- H1 закрыта фактически, не только по имени теста. Числовое
воспроизведение из CODE-REVIEW-249-r1 (
extra=1686.24,missing≈0) подтверждено моим независимым прогоном того же теста на коммите r1 — тест падает с точно той же величиной, значит новый тест действительно чувствителен к дефекту, а не переименован без содержания. На26fa968тот же тест зелёный с допуском1e-7в обе стороны плюс отдельная проверка, что каждая вершина пола остаётся внутри[100..900]×[100..700]. - Механизм фикса не подвержен тому же классу дефекта.
bevelMultiWallPaperи путьdifference(room.poly, roomGeom)только вычитают заранее вычисленные ограниченные фигуры — в отличие от старой веткиinsetContour/outsetContour, которая добавляла смещение по нормали без проверки соседнего ребра (корень H1).clipInnerContourToRoomна любом fallback-пути дополнительно пересекает результат сpr.poly, так что выход точки за пределы контура комнаты структурно исключён на уровне самой функции, а не только на happy path. - M1 закрыта содержательно. Новый кейс
[1.5, 5, 7]— три взаимно разные halfDepth — проходит через тот же строгий цикл (baseline/permutation/ production-scalecoordScale=1000/реальныйwallBodiesGeometryс fan-топологией из 3 комнат), что и остальные кейсы матрицы, без специального послабления. - AC4 (общий body для всех поверхностей) переподтверждён. Затронутые этой
дельтой пути —
bevelMultiWallPaper(paper) и clean-floor consumer вinnerContourForRoom— проверены смокомsmoke_multiwall_junction.mjs(paperRemainsSolid,cleanFloorConsumerIsPresent,planUsesCanonicalPath, parity Plan/View/kiosk/Static/hidden-Iso) — все зелёные. - 6 из 8 вызовов
innerContourForRoomвsrc/houseplan-card.ts(полный рендер: floor fills, room hover/labels, decor clip, glow/light) корректно пробрасываютthis._wallUnionGeometry()?.roomGeom— проверено построчно (grep -n "innerContourForRoom(", 8 вызовов, 6 с новым аргументом). - AC3 (двухлучевые узлы) дельтой не задета: путь для
multiWallNodes.nodes.length === 0не изменился (return insetнапрямую, как раньше); подтверждено тем, что полныйnpm testзелёный без правок существующих двухлучевых ожидаемых значений. - Трейлеры и changelog. Коммит
26fa968несётIssue: #249иUser-Visible: yes;docs/CHANGELOG.mdи.ru.mdправлены в этом же коммите. - Документация обновлена по существу, не формально:
WALL-THICKNESS.mdиARCHITECTURE.mdописывают именно новый механизм (roomGeom, вычитание, а не старую агрегированную triangle-diff схему) — сверил текст с кодом.
Находки
Блокирующих (High) нет. Medium в скоупе — нет.
L1 (Low, снимаю без правки, с записью). Два из восьми вызовов innerContourForRoom не получили sharedRoomWallGeometry
src/houseplan-card.ts:8502 (_rszEdgeLabels) и :8549 (_rszScaleLabels) —
подписи площади/размера во время живого drag resize/scale — вызывают
innerContourForRoom с неполным списком комнат (res.polys, только реально
подвинутые комнаты, либо вовсе один элемент для scale) и без пятого
аргумента. Проверил обе стороны эффекта:
- Корректность. Даже без
sharedRoomWallGeometry,innerContourForRoomсам вычисляетwallBodiesGeometry(rooms, ...)на этом же неполном списке и, если результат не подойдёт, уходит вclipInnerContourToRoom(inset, pr.poly)— обе ветки ограничены пересечением сpr.polyэтой же комнаты, поэтому именно дефект H1 (точка вне здания) здесь физически не воспроизводим: защита в самой функции универсальна, а не завязана на то, передан ли кэш. Возможное следствие неполного списка комнат — узел на границе с посторонней (не двигающейся) комнатой может быть на мгновение классифицирован как двухлучевой вместо 3+-лучевого в подписи площади во время drag; это не влияет на итоговую сохранённую геометрию и самоисправляется наpointerup, когда рендер снова читает полный_wallUnionGeometry(). - Производительность. Раз кэш не передан, а в комнатах есть узел
3+ (обычная ситуация, не экзотика — см. текст issue),
innerContourForRoomпересчитываетwallBodiesGeometry()заново при каждом вызове внутри_rszEdgeLabels/_rszScaleLabels, то есть на каждоеpointermoveво время resize-драга, отдельно от того, что тот же union уже пересчитывается для рендера стен через_wallUnionGeometry()(кэш которой инвалидируется на каждыйpointermove, потому что_rszApplyPreviewувеличивает_cfgEpochперед вызовом этих функций). Это дублирующая, но не новая по порядку величины нагрузка (сам рендер стен уже пересчитывает то же самое на каждый кадр драга) и касается только редакторского инструмента resize.
Не блокирую и не прошу правку в этом цикле: последствие ограничено overlay-
подписью во время интерактивного admin-only drag (§ SCOPE.md — редакторы вне
View), не влияет ни на один AC, самоисправляется по окончании драга, и
дополнительная нагрузка того же порядка, что уже существующий пересчёт стен
на каждый кадр. Снимаю как Low с этой записью; если автор захочет, дешёвое
улучшение — прокинуть this._wallUnionGeometry()?.roomGeom и в эти два
вызова, поскольку _curSpaceCfg/_spaceModel() в момент вызова уже отражает
живой _rszPreview (полный список комнат), в отличие от res.polys.
Чего не проверял
- Полный
npm run golden:verify/golden:accept, полный набор из 170 смоков, performance-профили, backend/HA-harness — предрелизные гейты по PROCESS.md §8, не обязаны быть прогнаны на этом заходе; риск R1 спеки («широкий blast radius для T-стыков») и в r1, и здесь требует полного golden-прогона перед бетой отдельно от этого цикла. - Не искал новых экземпляров L1-паттерна (неполный список комнат) вне
_rszEdgeLabels/_rszScaleLabels— это единственные два незатронутых дельтой вызова, остальные шесть проверены построчно. - Не гонял смоки, не входящие в прямые совпадения
smoke-selectдля этой дельты и не относящиеся напрямую к AC (smoke_split_corner_wall.mjs,smoke_wall_junctions.mjs— были зелёными в r1 на функции, которые эта дельта не меняла повторно; не перезапускал, доверяю r1 по §2.10, дельта их не касается). - Backend/Python — не тронут диффом.
Унаследовано из r1 (без повторной проверки)
Источник: docs/reviews/CODE-REVIEW-249-r1.md, вердикт по коммиту 062a98a.
- Продуктовая рамка и AC как формулировки (§7.1 ТЗ, разделы 1–9 спека) — дельта не меняет ни ТЗ, ни AC; полная перепроверка соответствия ТЗ пройдена в r1.
- AC5 (golden фиксирует видимый результат) — golden matrix/harness файлы
не тронуты дельтой r1→r2 (нет изменений в
demo/golden/**); вывод r1 «golden-сценарий синтаксически валиден, но заведомо не поймает H1 (fill_mode: 'none')» остаётся в силе и не пересматривался. - AC7 (гейты типа build/typecheck) — методика не изменилась; перепрогнаны заново в этом раунде (см. таблицу выше), не просто унаследованы со слов.
multiWallBevelTriangles/классификация узла (степень, epsilon, порядок, winding, production scale) вне нового M1-кейса — не переоценивались повторно вне того, что уже покрывает общийfor-цикл матрицы; сам механизм классификации (buildMultiWallNodeMap) дельтой не тронут.- Смоки
smoke_split_corner_wall.mjs,smoke_wall_junctions.mjs— зелёные результаты r1 приняты без повторного прогона: дельта не касается кода, который эти смоки покрывают (только H1/M1-специфичные пути). - Отсутствие влияния на backend/i18n/миграцию — подтверждено в r1 и не оспаривается дельтой (Python/i18n файлы не тронуты ни r1, ни r2).
Резюме
Обе блокирующие находки r1 закрыты предметно: H1 — новым путём вычисления
чистого пола (difference от кэшированной ограниченной канонической кладки, с
универсальным защитным клипом), проверенным через явную бисекцию «тест падает
на старом коммите с той же величиной, что называла находка, и зелёный на
новом»; M1 — буквальным кейсом трёх взаимно разных толщин, прогнанным через
тот же строгий матричный цикл. Новая находка L1 (Low) касается двух
call-site, не получивших кэш геометрии в живом resize-драге; она
структурно не может воспроизвести H1 (защита находится в самой функции) и
ограничена overlay-подписью во время admin-only интерактивного
инструмента — снимаю с запиской, без возврата на цикл.