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

20 KiB
Raw Blame History

CODE-REVIEW-261-r1

  • Issue: #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 на точном 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 «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): в нём также красный process-gate (exit code 2, «BEFORE_SHA 06a72dce... not a valid object» — типичный артефакт force-push после rebase, к коду не относится) и красный golden (тот же список different). Оба статуса устранились сами на следующем пуше — финальный Validate 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. Блокирующих находок нет.