docs: review document for #261

Issue: #261
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-23 13:13:14 +00:00
parent 0d0a9dd87a
commit 98dce8d0b4
+248
View File
@@ -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. Блокирующих находок нет.