Files
houseplan-card/docs/reviews/CODE-REVIEW-271-r1.md
T
2026-08-23 21:06:20 +03:00

254 lines
21 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-271-r1
- Issue: [#271](https://github.com/Matysh/houseplan-card/issues/271) — «Degree-3 узел достраивает короткий луч до 8×H»
- Этап: code (PROCESS.md §2.7)
- Заход: r1 (первый код-ревью цикл; спека прошла ревью зелёным в отдельном r1 — `docs/reviews/SPEC-REVIEW-271-r1.md`)
- Материал: `git log --oneline origin/dev..HEAD` = `396b391` (fix), `77143d4` (docs: спек-ревью), `482b2be` (docs: спека)
- Коммит реализации: `396b391a93cc228bd6b779b9985db87e853bfa50`, трейлеры `Issue: #271` / `User-Visible: yes`, оба changelog в этом же коммите — корректно.
- Вердикт: **красный** (High: 1, Medium: 1 в скоупе)
## Скоуп
Диапазон `origin/dev...HEAD` — один продуктовый файл `src/wall-thickness.ts`
(добавлено поле `supports: MultiWallNodeRaySupport[]` на канонический луч
multi-wall узла и ограничение локальной реконструкции реальной длиной
support'а вместо `8×H`), плюс тесты (`test/wall-thickness.test.mjs`,
`test/golden-matrix.test.mjs`), таргетированный smoke
(`demo/smoke_junction_patch_resilience.mjs`), golden matrix/harness
(`demo/golden/matrix.mjs`, `demo/golden/harness.mjs`), мутация
(`scripts/mutation-gate.mjs`), реестр smoke-связей (`scripts/smoke-links.mjs`)
и документация (ARCHITECTURE/WALL-THICKNESS/TESTING/USER-GUIDE×2/CHANGELOG×2 +
`docs/images/screenshots.json`). Соответствует заявленным «Ожидаемым файлам»
§9 ТЗ.
Привязка к продукту подтверждена: J1 (план не должен показывать
несуществующую архитектуру) и J6 (одна каноническая геометрия для всех
потребителей) — сценарий и симптом (`docs/SCOPE.md`).
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | зелёный, без вывода |
| Юнит | `npm test` | 1163 passed, 0 failed |
| Сборка | `npm run build` | зелёный |
| Синхронизация трёх копий бандла | `npm run bundle:sync` | зелёный; `git status --short` после — пусто (байт-в-байт) |
| Документация/скриншот-отпечаток | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» |
| Целевая мутация | `node scripts/mutation-gate.mjs --id=multi-wall-finite-ray-disabled` | поймано 1/1, чистый прогон зелёный |
| Выбор smoke по дельте | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | назвал `smoke_junction_patch_resilience.mjs`, `smoke_multiwall_junction.mjs` (обе — «зарегистрированная связь») |
| Smoke #197/#271 | `node demo/smoke_junction_patch_resilience.mjs` | OK, все поля true, включая новые `*StopsAtFiniteRayEndpoint` |
| Smoke #249 | `node demo/smoke_multiwall_junction.mjs` | OK |
| **Целевой semantic golden (AC6)** | `node demo/golden/run.mjs --mode=capture --scenario=junction-patch-resilience-plan-dark` | **error**: `golden finite multi-wall ray contract failed` |
| **Целевой semantic golden (AC6)** | `node demo/golden/run.mjs --mode=capture --scenario=junction-patch-resilience-view-dark` | **error**: та же ошибка |
| Независимая проверка чтением | прямой вызов `buildMultiWallNodeMap`/`wallBodiesGeometry` из `test-build/wall-thickness.js` на той же фикстуре | подтвердил причину ошибки на уровне сырой геометрии (см. находку H-1) |
`npm run golden:verify` не запускался — инструмент сам это запрещает вне
полной матрицы (`policy.mjs: golden verify must run the complete matrix`),
что и является предусмотренным prerelease-гейтом (§8/AC9). Но именно поэтому
диагностический `--mode=capture --scenario=<id>` — единственный способ
исполнить добавленную семантическую проверку AC6 локально, и он был обязателен
для этих двух конкретных сценариев, потому что diff добавляет в них новую
проверку (`absentWallProbes`) целиком под этот issue.
## Находки
### [High] AC6 не выполняется: собственная семантическая golden-проверка issue падает на обоих сценариях
**Файлы:** `demo/golden/matrix.mjs:266,271` (значение `absentWallProbes[1]`),
`demo/golden/harness.mjs:582-588` (проверка, которая на нём падает).
**Воспроизведение:**
```
node demo/golden/run.mjs --mode=capture --scenario=junction-patch-resilience-plan-dark
# → error junction-patch-resilience-plan-dark
# artifacts/golden/golden-report.json:
# "error": "page.evaluate: Error: golden finite multi-wall ray contract
# failed: junction-patch-resilience-plan-dark"
```
То же для `junction-patch-resilience-view-dark`. Точечной проверкой (клонирую
сценарий с одним `absentWallProbes` элементом за раз) показано, что падает
именно второй пробник — `[0.936524285, 0.345833333]`; первый
(`[0.420833333, 0.37625]`, тот же, что в
`smoke_junction_patch_resilience.mjs`) действительно пуст.
**Причина (подтверждена прямым вызовом продукт-функций на той же фикстуре
`test/fixtures/197-junction-patch.json`, без браузера):**
Узел `(887.5, 345.833)` (в render units) имеет три луча:
```
u=[1,0] halfDepth=6.25 length=1.3819039326657503 ← искомый короткий луч из таблицы issue
u=[-1,0] halfDepth=9.1667 length=195.833...
u=[0,-1] halfDepth=12.083 length=295.833...
```
Это в точности совпадает со строкой из issue/ТЗ:
`1 / (887.500, 345.833) | 1.381904 | 96.667` — реализация действительно
ограничивает этот луч `1.3819`, а не `96.667`. Сам фикс на этом узле работает
правильно.
Но зонд `[936.524285, 345.833]` находится в `887.5 + 49.02` — то есть внутри
совершенно другого, независимого и полностью легитимного интервала стены:
```
room_8: a=[888.8819039326658, 345.8333], b=[979.1666666666666, 345.8333],
cm=22, half=9.1666...
```
Этот интервал начинается сразу там же, где заканчивается короткий луч
(`888.88`), и тянется до `979.17` — то есть физически это настоящая,
непрерывная (без зазора) кладка `room_8`, никак не связанная с 8×H-багом.
Прямой point-in-polygon расчёт против `wallBodiesGeometry(...).geom`
подтверждает: `probe1 → true` (внутри), `probe0 → false` (пусто, как и
ожидается).
Значит: между реальным концом короткого луча (`1.3819`) и следующим
независимым сегментом (`888.88`) зазора попросту нет — вдоль этого
направления в принципе не существует точки, которая была бы одновременно
«дальше короткого луча» и «пуста», потому что там сразу начинается другая
настоящая стена. §6.3 ТЗ прямо предвидел этот случай («если другой
независимый ray/body законно занимает точку за этим торцом, final union
остаётся заполненным им») — но зонд был выбран без проверки этого условия.
**Почему это блокирует:** AC6 требует, чтобы «до pixel diff harness
проверял, что два probes за finite endpoint пусты» — с добавленным кодом
это происходит, и один из двух пробников красный. Это не спекуляция и не
пропущенный гейт: гейт исполнялся (дважды, разными методами — Chromium и
прямой вызов), и оба раза воспроизвёл одну и ту же ошибку. Если это попадёт
в предрелизный `npm run golden:verify` (§8), CI-джоб `golden` станет красным
на `dev` до следующей задачи — тот же класс инцидента, что уже стоил
продукту #237. Хендофф-комментарий автора не упоминает запуск этого сценария
вообще (перечислены typecheck/test/bundle:sync/check-docs/mutation-gate/два
smoke) — гейт добавлен в этот же коммит, но не был исполнен ни разу.
**Что нужно исправить:** заменить `absentWallProbes[1]` на координату,
которая действительно лежит в пустоте за реальным концом искомого короткого
луча (либо выбрать другой узел/направление для второй иллюстрации из
таблицы issue, для которого такая пустая точка вообще существует — у этого
конкретного направления её нет, зазора между сегментами нет). Требуется
перепрогнать оба golden-сценария до зелёного после правки.
### [Medium, в скоупе] AC4 не имеет собственного fixture/smoke с проёмом
ТЗ (§8, AC4) и сама формулировка issue («Обязательные регресс-тесты», п. 3)
требуют fixture с коротким перпендикулярным лучом рядом с дверью и
targeted browser smoke с реальным SVG/shadow probe, доказывающие: opening
slot остаётся пустым, короткий луч не заходит в slot, light/sun occluder не
получает лишней площади, а opening tunnel/symbol contract не меняется.
Ни новый unit-тест (`test/wall-thickness.test.mjs:840`, `openings: []`), ни
переиспользуемая фикстура `test/fixtures/197-junction-patch.json`
(`openings` отсутствует как ключ), ни `test/fixtures/249-multiwall-junction.json`
не содержат ни одного проёма. Единственная косвенная проверка —
`lightAndSunStopAtFiniteRayEndpoint` в `smoke_junction_patch_resilience.mjs`
— проверяет тот же общий occluder-механизм, но не рядом с дверью и без
проверки самого opening slot/tunnel/symbol.
**Проверено чтением, не исполнением:** `bevelMultiWallBody()` вызывается в
`wallBodiesGeometry()` (`src/wall-thickness.ts:2686`) строго до цикла вырезания
проёмов (`src/wall-thickness.ts:2688-2705`), который работает как чистое
булево вычитание по `openingWallIndex`, построенному из `walls`/`wallIntervals`
— эта функция в диффе не тронута. Значит вырезание проёма не может вести
себя иначе только потому, что рядом finite ray; фикс равно применяется вне
зависимости от наличия проёма поблизости, и вероятность отдельного,
специфичного для проёма дефекта архитектурно мала. Именно поэтому это
Medium, а не High: живого дефекта не нахожу, но обязательное по своему же ТЗ
регресс-покрытие именно того производственного симптома, который запустил
issue (тёмное пятно у двери на `2-1.png`), отсутствует.
В скоупе задачи, чинится в этом же цикле по правилу §2.7 (Medium без High —
жёлтый; здесь High уже есть, поэтому общий вердикт красный, но это не меняет
маршрут находки — не заводится отдельным issue).
## Что проверено и корректно
- **AC1** (node map хранит конечную длину, permutation/order-independent) —
доказано автотестом `test/wall-thickness.test.mjs:840` (deep-equal supports,
reversed/permuted intervals дают одинаковую подпись). Тест умеет падать:
мутация `multi-wall-finite-ray-disabled` красит именно этот тест-файл
(через ту же комбинированную проверку, что и AC2, — см. ниже).
- **AC2** (короткий луч не достраивается) — доказано автотестом и
независимо — прямым вызовом `buildMultiWallNodeMap`/`wallBodiesGeometry` я
воспроизвёл узел `(887.5,345.833)` и подтвердил `length=1.3819...`, а не
`96.667`; probe `[0,10]` внутри, `[0,100]` и `roomGeom`-эквивалент — снаружи
(тест). Совпадает с числами из таблицы issue.
- **AC3** (длинные rays/join #249 не обрезаны) — весь существующий набор
`#249`/`#261`/`#197` зелёный; area-бейзлайн `#197`-фикстуры пересчитан с
явным комментарием, объясняющим уменьшение (`124568.27...→124244.27...`),
величина уменьшения (≈324 ед²) правдоподобна для одного убранного фантомного
прямоугольника.
- **AC5** (все поверхности используют finite result) — доказано smoke:
`planStopsAtFiniteRayEndpoint`, `viewStopsAtFiniteRayEndpoint`,
`kioskStopsAtFiniteRayEndpoint`, `staticStopsAtFiniteRayEndpoint`,
`hiddenIsoStopsAtFiniteRayEndpoint`, `lightAndSunStopAtFiniteRayEndpoint`,
`cleanFloorOwnsAreaAfterFiniteEndpoint` — все `true` при реальном прогоне в
Chromium.
- **AC7** (мутант ловит регрессию) — `node scripts/mutation-gate.mjs
--id=multi-wall-finite-ray-disabled` подтверждает: чистый прогон зелёный,
патч `supportExtent = extent` красит тест. Мутация задевает ровно ту
строку (`src/wall-thickness.ts` в `bevelMultiWallBody`), которая
реализует контракт §6.1/6.3 ТЗ.
- **AC8** (приватность/детерминизм)** — проверено чтением: fixture —
переиспользование уже прошедшей ревью анонимизированной `197-junction-patch.json`,
приватные `1.json`/`2.json` не коммитятся (в диффе их нет);
`renderNeverWritesConfig` в smoke подтверждает отсутствие мутации config.
- **Failure isolation §6.5** — проверено чтением: `try/catch` вокруг цикла
по узлам в `bevelMultiWallBody` не тронут, `canonicalRays`-фильтр
(`Number.isFinite && > 0/eps`) исключает только невалидный ray/node, не
весь план.
- **Compatibility §7** — persisted config/model version не тронуты (diff
ограничен внутренним представлением `MultiWallNodeRay`/локальной
реконструкцией); нет нового глобального `O(E²)` — `canonicalSupports`им
`O(k²)` только по числу дублей на одном направлении одного узла, что и
раньше было O(дублей) на дедупликации.
- **Трейлеры и changelog** — один коммит `396b391`, `Issue: #271`,
`User-Visible: yes`, оба changelog внутри того же коммита — соответствует
§7 PROCESS.
- **Документация** — `ARCHITECTURE.md`/`WALL-THICKNESS.md`/`TESTING.md`/оба
`USER-GUIDE` обновлены точно в терминах реализованного контракта (non-dominated
finite `(half-depth, length)` supports); `docs/images/screenshots.json` —
только `sourceFingerprint` изменился, все `imageSha256` совпадают (PNG не
тронуты) — подтверждено `check-docs.mjs` и сверкой diff вручную.
- **smoke-links** — `scripts/smoke-links.mjs` дополнен новыми символами
(`buildMultiWallNodeMap`, `MultiWallNodeRay`, `MultiWallNodeRaySupport`) с
причиной; `smoke-select` подтвердил именно эти два smoke как
«зарегистрированная связь», оба фактически прогнаны.
## Чего не проверял и почему
- **Полный `npm run golden:verify`** — инструмент сам требует полную матрицу
(не принимает `--scenario` в verify-режиме); это осознанный prerelease-гейт
по AC9/§8, не подмена. Вместо этого исполнен диагностический
`--mode=capture --scenario=<id>` для обоих сценариев, добавленных этим
диффом — что и вскрыло находку H-1.
- **Полная smoke-матрица (174 сценария)** — не запускал; `smoke-select`
назвал только 2 релевантных, оба прогнаны. Diff не расширяет поверхность
за пределы multi-wall reconstruction, широких символов (>34 смоков) не
затронуто.
- **`npm run invariants`** — не запускал. Diff не трогает хранение записей
толщины, `layout`, `marker.space`, `open_spans` или ключи решёточных рёбер
— только внутреннее представление `MultiWallNodeRay` и локальную
реконструкцию тела в уже существующей структурной фазе, читающую уже
разрешённые `WallInterval`. Ссылочная целостность конфигурации не может
быть задета этим диффом; проверено чтением diff (`git diff --stat`
показывает единственный продуктовый файл `src/wall-thickness.ts`, без
затрагивания `setWallThickness`/хранения ключей).
- **`python -m pytest tests_backend`** — не запускал, `custom_components/**/*.py`
не тронут (только скомпилированный `frontend/houseplan-card.js`).
- **Performance-профили** — не названы в AC, geometry hot-path не меняет
алгоритмической сложности (см. Compatibility выше).
- **Полный golden/perf/Linux HA harness** — предрелизный гейт по PROCESS
§8/AC9 «Release-артефакты» ТЗ, сознательно не гоняется в цикле ревью.
## Итог
Реализация верно устраняет корневую причину (потерю длины луча в
`buildMultiWallNodeMap`/`bevelMultiWallBody`) и большинство AC подтверждены
автотестами и независимым чтением/пересчётом. Но собственная семантическая
golden-проверка, которую сам диф добавляет для AC6, красная при реальном
исполнении — это High, блокирует. Плюс отсутствует обязательное по ТЗ
door-adjacent регресс-покрытие AC4 — Medium, в скоупе. Оба возвращаются
автору в этом же цикле, отдельный issue не заводится.