Files
houseplan-card/docs/reviews/CODE-REVIEW-261-r1.md
T
2026-08-23 13:13:14 +00:00

249 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. Блокирующих находок нет.