From 98dce8d0b4aeb6e4c7375cb81739c88503f9069b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:13:14 +0000 Subject: [PATCH] docs: review document for #261 Issue: #261 User-Visible: no --- docs/reviews/CODE-REVIEW-261-r1.md | 248 +++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 docs/reviews/CODE-REVIEW-261-r1.md diff --git a/docs/reviews/CODE-REVIEW-261-r1.md b/docs/reviews/CODE-REVIEW-261-r1.md new file mode 100644 index 00000000..0c864cef --- /dev/null +++ b/docs/reviews/CODE-REVIEW-261-r1.md @@ -0,0 +1,248 @@ +# CODE-REVIEW-261-r1 + +- Issue: [#261](https://github.com/Matysh/houseplan-card/issues/261) — реальная причина белых клиньев в T-стыках +- Этап: code · заход r1 (первый код-раунд; ТЗ прошло независимое ревью зелёным, + `docs/reviews/SPEC-REVIEW-261-r1.md`, коммит ТЗ `96cd2f2`) +- Ветка: `issue/261-white-wedges-root-cause`, ребейзнута на `origin/dev` (`18d5046`) +- Диапазон: `git diff origin/dev...HEAD` (base `18d5046` → head `0d0a9dd`) +- Продуктовый коммит: `fix: retain bounded T-junction masonry` (`0dc6c39`, + `User-Visible: yes`); доказательства/документация — `2e53cb0`, `4eda889`, `0d0a9dd` + (`User-Visible: no`) + +## Скоуп + +Диагноз ТЗ: `bevelMultiWallPaper()`/`bevelMultiWallBody()` в +`src/wall-thickness.ts` резали физическое пересечение лучей T-стыка от +исходных offset-origins (`multiWallBevelTrianglesAt(map, false)`), а затем +клипали локальную реконструкцию только к room-centre union — теряя +допустимую наружную половину стены вплоть до `1.25 × H`. Контракт ТЗ: +вернуть эту область, не восстанавливая старый неограниченный mitre-зуб +#249, на всех потребителях канонической геометрии. + +Фактический diff — ровно это и только это: + +- `src/wall-thickness.ts` (+9/-7 строк, только логика): + - `bevelMultiWallPaper()`: `multiWallBevelTrianglesAt(map, false)` → + `multiWallBevelTriangles(map)` (тот же bounded-хелпер, что уже + использует masonry) — paper больше не начинает cut от offset origins; + - `bevelMultiWallBody()`: порядок клипа `localInside` инвертирован — + `if (envelope) ... else if (centre) ...` вместо + `if (centre) ... else if (envelope) ...`. `envelope` — это + `paperGeom`, всегда передаваемый вызовом на `wall-thickness.ts:2632` + вместе с `centre`, поэтому раньше `envelope` фактически не работал: + `centre` (уже) всегда перехватывал ветку раньше. +- Тесты/смоки/golden/mutation-gate/документация/changelog — расширены + под новый контракт, продуктовой логики не касаются. + +Никакой другой файл `src/**` не тронут; config/schema/model_version, +backend и i18n вне diff — совпадает с §5/§7 ТЗ. + +## Как проверялось + +Материал — `git diff origin/dev...HEAD` и построчное чтение изменённых +функций (`multiWallBevelTrianglesAt`, `multiWallBevelTriangles`, +`bevelMultiWallBody`, `bevelMultiWallPaper`) с прослеживанием обоих call +site (`wall-thickness.ts:2590`, `:2632`). Ручного запуска приложения не +было — только автогейты и чтение. + +Гейты — что прогнал и что получил: + +| Гейт | Результат | +|---|---| +| `npx tsc --noEmit` | green, без вывода | +| `npm test` | `1159 passed, 0 failed` (у автора `1158/1 skip` — расхождение только в счётчике skip/pass, не в провалах) | +| `npm run bundle:sync` | green; `dist/houseplan-card.js`, `custom_components/houseplan/frontend/houseplan-card.js`, `demo/srv/assets/houseplan-card.js` идентичны, `git status` чист после пересборки | +| `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` | +| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 2 «зарегистрированные связи»: `smoke_junction_patch_resilience.mjs`, `smoke_multiwall_junction.mjs` — обе названы в AC2/AC4, обе прогнаны | +| `node demo/smoke_junction_patch_resilience.mjs` | **22/22 проверок green**, включая все новые `*RetainsMeasuredWedge`/`cleanFloorExcludesMeasuredWedge` поля (Plan/View/kiosk/Static/hidden Iso/light barrier/paper/clean floor) | +| `node demo/smoke_multiwall_junction.mjs` | **15/15 green**, включая `excessWedgeIsEmpty: true` — старый #249 clamp жив | +| `node scripts/mutation-gate.mjs --id=multi-wall-paper-full-origin-cut` | мутант пойман: `issue #197 keeps...` тест краснеет при возврате full-origin cut и старого порядка `centre`/`envelope` | +| `node scripts/model-invariants.mjs --config test/fixtures/197-junction-patch.json` и `.../249-multiwall-junction.json` | оба green («ссылки разрешимы, записи толщины находятся») | +| `npm run golden:verify` | полный прогон (`verify` игнорирует `--only` и всегда гоняет весь матрикс). Формально код возврата ненулевой (см. разбор ниже), но по существу: **обе целевые сцены `junction-patch-resilience-plan-dark`/`-view-dark` — `passed`**; 4 `different` + 1 `missing-baseline` — отдельно разобраны ниже, это не регрессия этого diff | + +Не прогонял и почему: + +- полный `demo/smoke_*.mjs` (кроме двух названных) — задача не трогает + ничего вне `bevelMultiWallBody`/`bevelMultiWallPaper`; полный набор — + предрелизный гейт (PROCESS.md §8); +- performance smoke — не назван в AC, structural cache pass не тронут + (строится один раз, как и раньше; diff не меняет частоту перестроения); +- `python -m pytest tests_backend` — diff не касается `custom_components/**/*.py`; +- `model-invariants` на реальном экспорте владельца + (`houseplan-space-1-2026-08-23_02-30-42.json`) — файл существует только + на машине автора (Windows-путь `C:\Temp\...`), в этом окружении + недоступен. Прогнал ту же команду на обеих релевантных фикстурах + (#197, #249) вместо него — обе green. Отчёт автора об этом конкретном + прогоне проверить своими силами не могу; +- формальное независимое принятие Linux golden baseline + (`golden:accept -- --reviewed`) — не мой шаг в этом раунде. + +### Разбор `npm run golden:verify`: 4 different + 1 missing-baseline — не регрессия + +`goldenRunFailed()` (`demo/golden/policy.mjs:21`) в режиме `verify` +считает провалом любой статус, кроме `passed`, поэтому итоговый код +выхода ненулевой и у меня, и в CI (мой первый прогон через `| tail -100` +маскировал реальный код возврата — сам `npm run golden:verify` красный, +как и должен быть при наличии `different`). + +Проверил, что это не регрессия этого diff, а существующий на `dev` долг: + +1. поднял `git worktree` на `origin/dev` (`18d5046`, ровно та точка, от + которой ветка ребейзнута), собрал (`npm run bundle:sync`) и прогнал + `npm run golden:verify` — получил **тот же набор**: `different` на + `isometric-large-warm-remount-dark`, `large-house-zoom-040-dark`, + `large-house-zoom-250-dark`, `large-house-warm-remount-dark` и + `missing-baseline` на `wall-key-roundtrip-view-dark`; +2. независимо подтвердил тем же результатом свежий CI-прогон `dev` — + [Validate 32639712334](https://github.com/Matysh/houseplan-card/actions/runs/32639712334) + на точном `18d5046`: `golden` job красный с идентичным списком + (`different`×4 + `missing-baseline`×1, exit code 1); +3. `wall-key-roundtrip-view-dark` — известный `missing-baseline` от уже + слитого #258, не относится к этой задаче; `large-house-*`/`isometric-large-*` + — сцены на фикстурах `demo/fixtures/large-house.mjs`/`visual-matrix.mjs`, + чей wall-key не разбирается штатным `lookupWall` (открытый + [#260](https://github.com/Matysh/houseplan-card/issues/260) + «golden и перф-бюджет мерят не то»), то есть эти сцены уже измеряют + не ту геометрию независимо от #261 — новый issue не нужен, долг уже + заведён и открыт. + +Обе цели AC5 (`junction-patch-resilience-plan-dark`/`-view-dark`) — +`passed` и на `dev`, и на `HEAD`; каждая отдельно содержит встроенную +семантическую проверку (`retainedWedgeProbe` в +`demo/golden/harness.mjs:154,570`, добавленную этим diff'ом) — она бы +бросила исключение и уронила весь прогон, а не просто пометила сцену +`different`, если бы клин остался пустым. То, что итоговый статус этих +двух сцен — `passed`, а не `different`, само по себе показательно: +восстановленная площадь (55.38 render-unit² из ~727 тыс. общей площади +paper, ~0.008%) физически меньше порога `maxDiffRatio` матрицы +(0.05–0.1%) — обычный попиксельный diff сам по себе эту регрессию бы не +поймал. Это ровно то, ради чего AC5 ТЗ требует семантический probe, а не +только пиксельное сравнение, и объясняет, почему регрессия #261 могла +остаться необнаруженной так долго. + +Отдельно посмотрел (как изображение) `artifacts/golden/diff/*` для двух +`large-house-*` сцен: помимо фонового шума по хэтч-паттерну на каждой +стене (ожидаемо — реконструкция задевает каждый degree-3+ узел плана, +включая узлы `large-house`), в обеих виден один явно больший +восстановленный треугольник у одного T-стыка — по форме и характеру то +же исправление, что и в целевой фикстуре #197, на другой геометрии. Не +похоже на посторонний дефект. + +Также посмотрел на CI-прогон, который автор процитировал как источник +golden-артефакта +([32639857405](https://github.com/Matysh/houseplan-card/actions/runs/32639857405)): +в нём также красный `process-gate` (`exit code 2`, «BEFORE_SHA +06a72dce... not a valid object» — типичный артефакт force-push после +rebase, к коду не относится) и красный `golden` (тот же список +different). Оба статуса устранились сами на следующем пуше — финальный +[Validate 32640136740](https://github.com/Matysh/houseplan-card/actions/runs/32640136740) +на `0d0a9dd` зелёный. `frontend`/`smoke`/`golden`/`backend` в этом +финальном прогоне пропущены механизмом дедупликации (#208, «входы +побайтово те же, что в предыдущем успешном прогоне») — зелёная галочка +Validate на финальном коммите не значит, что golden реально перепрогнан +именно на нём. Для этого раунда это не проблема: golden я прогнал сам, +руками, на точном `HEAD`. + +## Находки + +Одна Low, не блокирует и не требует правки: + +- **Low.** `src/wall-thickness.ts:2090-2091` — после инверсии порядка + `if (envelope) ... else if (centre) ...` параметр `centre` в этой + конкретной ветке (`localInside`) стал недостижим при единственном + существующем вызове `bevelMultiWallBody` (`wall-thickness.ts:2632`), + потому что туда всегда передаётся непустой `envelope` (`paperGeom`). + `centre` остаётся использоваться чуть ниже — в `preservedExterior` — + так что это не мёртвый параметр функции целиком, только один из двух + путей внутри неё. Функционально безопасно, тесты и смоки это + подтверждают. Не требует правки в этом раунде. + +Ни одной находки, влияющей на AC или требующей возврата автору. + +## Что проверено и корректно + +- **AC1** (T-узел сохраняет физический сектор) — `test/wall-thickness.test.mjs` + добавляет geometry-based (не строковые) проверки: `affectedNode.rays.length + === 3`, отсутствие узла на нулевой грани, `assertProbeInside` на + `roomGeom`/`geom`/`paperGeom` в измеренной точке `(895.5, 556)`, + area-connectivity для каждого incident ray, `assertProbeOutside` на + clean floor каждой комнаты. Площади (`124568.27047237023` / + `727303.8153386558`) отличаются от старых (`124512.89263371378` / + `727248.4374999999`) ровно на `55.38`/`55.38` render-unit² — совпадает + с измеренным в ТЗ (§2) исчезнувшим треугольником `55.3819444449` + (расхождение в шестом знаке — округление на границе маски, не + тревожит). Убедился чтением, что тест использует реальный `polygon + area`/`point coverage`, а не голую непустоту. +- **AC2** (#249 не ослаблен) — блок `test/wall-thickness.test.mjs:758-956` + (bound `1.25×H`, permutation/order/scale independence, старый + excessive wedge) не изменён этим diff'ом и остаётся зелёным в + `npm test`; отдельно `excessWedgeIsEmpty: true` в + `smoke_multiwall_junction.mjs`. +- **AC3** (#197/#258/zero-depth) — существующие regression-тесты не + переписаны (только дополнены), zero-depth-грань `(620.833,545.833)` + явно проверяется как не входящая в degree-3 карту; новый мутант + `multi-wall-paper-full-origin-cut` откатывает ровно оба изменённых + места (paper helper + порядок `centre`/`envelope`) одним патчем и + ловится — проверено запуском (`поймано 1 из 1`), то есть тест умеет + падать. +- **AC4** (все browser-поверхности) — 22/22 проверок + `smoke_junction_patch_resilience.mjs` зелёные, включая раздельные + probe для Plan/paper/View/kiosk/Static/hidden Iso/light barrier и + отрицательную проверку clean floor через реальный + `isPointInFill()`/point-in-ring, не только непустоту path. +- **AC5** (golden защищает клин семантически) — `retainedWedgeProbe` + добавлен в обе сцены и в `harness.mjs` как обязательный, кидающий + исключение при незаполненном probe; `test/golden-matrix.test.mjs` + проверяет наличие поля. Оба сценария `passed` при полном + `golden:verify` (разбор дрейфа остальных сцен — выше, это не + регрессия diff'а). +- **AC6** (данные/performance не меняются) — diff ограничен + `src/wall-thickness.ts`; structural pass по-прежнему строится один + раз на рендер (`stateTickReusesGeometry`/`stateTickKeepsBarrierFingerprint` + в смоке зелёные); `model-invariants` green на обеих доступных + фикстурах. +- **AC7** (гейты) — все обязательные локальные гейты из списка ТЗ + прогнаны мной независимо и зелёные (таблица выше); полные + smoke/performance подтверждены как prerelease-гейты вне этого + раунда, как и требует ТЗ. +- **Трейлеры и changelog** — `Issue: #261` на каждом коммите; + `User-Visible: yes` только на `0dc6c39`, и именно в нём же лежат + правки `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` — формулировки точно + описывают наблюдаемое поведение («no longer show white triangular + gaps», «without restoring the former long mitre spike»), без + терминов реализации. `docs/USER-GUIDE.md`/`.ru.md` синхронно + обновлены тем же коммитом. +- **Документация подсистемы** — `docs/WALL-THICKNESS.md` и + `docs/ARCHITECTURE.md` переформулированы точно под новый код (envelope + вместо room-centre union, bounded cut для paper), а не оставлены + устаревшими; `docs/TESTING.md` описывает новый мутант и golden-probe. +- **Одно число — один источник** — diff не добавляет и не меняет ни + одной пользовательски видимой величины (площадь, толщина, подпись); + это чисто геометрический рендер-фикс без новых чисел в UI. Проверка + неприменима. +- Config/layout/`model_version`, i18n, backend, security surface — вне + diff, подтверждено по `git diff --stat`. + +## Чего не проверял + +- Реальный экспорт владельца (`model-invariants` на нём) — файл + недоступен в этом окружении; ограничился фикстурами #197/#249. +- Полный `demo/smoke_*.mjs` набор и performance smoke — не требуются + для этого diff (см. таблицу выше), остаются prerelease-гейтом. +- Формальное независимое принятие Linux golden baseline + (`golden:accept -- --reviewed`) — не мой шаг в этом раунде; я + подтвердил лишь, что дрейф не новый и целевые AC5-сцены проходят + семантически. +- Ручное открытие приложения в браузере (не запускал dev server) — + весь визуальный вывод проверен через smoke/golden headless-прогоны, + а не глазами на живом UI. + +## Вывод + +Диагноз, диапазон правки и объём тестового доказательства точно +совпадают с ТЗ. Единственное изменение — два места в +`bevelMultiWallBody`/`bevelMultiWallPaper`, обе точки воспроизводятся +мутационным гейтом и покрыты geometry-based (не строковыми) +assertions на всех перечисленных в ТЗ потребителях. Golden red — +целиком объяснённый пред-существующий долг `dev` (issue #260), не +регрессия этого diff. Блокирующих находок нет.