Files
houseplan-card/docs/reviews/CODE-REVIEW-288-r2.md
T
2026-08-24 15:57:49 +03:00

226 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-288-r2
- **Issue:** https://github.com/Matysh/houseplan-card/issues/288
- **Этап:** code (PROCESS.md §2.7)
- **Заход:** r2 · блокирующих циклов израсходовано 1 из 4
- **Ветка:** `issue/288-bounded-multiwall-corridor`
- **HEAD на момент ревью:** `162f9b0eca8f61b4ae1d716785f1f94f8d448acc`
- **Диапазон:** `origin/dev...HEAD`
## Почему разбор полный, а не по дельте
Раунд формально второй код-ревью, но материал был перепроверен полностью, а
не только по видимой дельте — по правилу PROCESS.md §7.2 «после ребейза это
другой код». Доказательство, а не предположение:
- CODE-REVIEW-288-r1.md называет SHA на момент r1: `4aa79dee6b1e...`
(коммит «docs: accept screenshots after junction repair»), а fix-коммит
r1 — `ff5e393c`, спецификацию — `cb0f9e73`/`fba615e9`. Сам вердикт-комментарий
в issue эти SHA не называет — это тоже находка процесса, отдельно отмечена
ниже.
- Ни один из этих объектов не существует в текущем репозитории:
`git cat-file -e 4aa79dee6b1e66946c9fb97ee38fc855b0020808` → отсутствует;
`9031ba0d`, `ff5e393c`, `cb0f9e73` → `fatal: Not a valid object name`.
- Причина: между r1 и r2 ветка `issue/260-fixture-wall-keys` была доведена до
готовности и слита в `dev` (`git merge-base --is-ancestor
origin/issue/260-fixture-wall-keys origin/dev` → предок, тогда как в r1
`9031ba0d` предком `origin/dev` не был), а `issue/288-...` была перестроена
на новый `dev` — отсюда новые хеши у всех коммитов диапазона (спека,
ревью-документы, fix, пересъёмка скриншотов). Премьерный коммит пересъёмки
`4aa79dee` («after junction repair») заменён на `162f9b0e` («after fixture
repair») — другое сообщение, другое содержимое `screenshots.json`
(`sourceFingerprint` пересчитан).
- Ребейз не декоративный: `demo/smoke_real_plan_masonry.mjs` и обе копии
бандла входят и в стек `#260`, и в fix-коммит `#288` (`comm -12` между
списками файлов даёт непустое пересечение) — то есть после ребейза fix
накладывался на файл, уже изменённый `#260` на 71 строку. Контент нужно
было перепроверять заново, а не считать «тем же diff под новым именем».
Итог: пункт 3 инструкции («по каждой находке предыдущего раунда покажи, чем
именно она закрыта») выполнен, но объём проверки — полный §2.7, включая
живые прогоны всех гейтов заново на текущем HEAD.
## Скоуп диффа
`git diff origin/dev...HEAD --stat`: `src/wall-thickness.ts` (+134/-6, ядро
фикса), `test/wall-thickness.test.mjs` (+51, AC3), `scripts/mutation-gate.mjs`
(+14, AC7), `demo/smoke_real_plan_masonry.mjs` (числа `PLANS`/`KNOWN_GAP_*`
обновлены на нули), `docs/{WALL-THICKNESS,ARCHITECTURE,TESTING,CHANGELOG,
CHANGELOG.ru}.md`, 1 PNG + `docs/images/screenshots.json` (пересъёмка после
слияния #260), `docs/reviews/{SPEC-REVIEW-288-r1,SPEC-REVIEW-288-r2,
CODE-REVIEW-288-r1}.md` (артефакты предыдущих раундов), `dist/houseplan-card.js`
и `custom_components/houseplan/frontend/houseplan-card.js` (сгенерированные,
класс D).
Ядро изменения не поменялось относительно описанного в CODE-REVIEW-288-r1:
`buildMultiWallNodeMap` добавляет каждому `MultiWallNodeRay` поле
`continuations` — конечные общие полосы («shared»-стены), примыкающие к
дальнему концу одного из `supports` луча. `bevelMultiWallBody` восстанавливает
эти полосы (`multiWallContinuationStripGeometry`) внутри маски узла и
`union`-ит результат вместо старой тройки `outside/preservedExterior/
localInside`.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| **Medium** — пересъёмка docs-скриншотов состоялась раньше слияния `issue/260-fixture-wall-keys` в `dev`, нарушая явный порядок из issue и §10 спеки | `#260` слит в `dev` (доведён до нового tip), ветка `#288` перестроена на этот `dev`, скриншоты пересняты повторно уже после слияния | `git merge-base --is-ancestor origin/issue/260-fixture-wall-keys origin/dev` → предок (в r1 было «не предок»); коммит `162f9b0e` «docs: recapture screenshots after fixture repair» — самый верхний коммит диапазона, идёт строго после того, как `52722876` (tip `#260`) стал предком всей цепочки; `node scripts/check-docs.mjs` зелёный на текущем `sourceFingerprint` |
| **Low** — юнит AC3 проверяет только метаданные `ray.continuations`, не строит `wallBodiesGeometry` и не измеряет фактическую потерю площади | Не тронуто этим диапазоном (файл `test/wall-thickness.test.mjs` не менялся после r1) — находка остаётся открытой, но не блокирует; компенсируется независимо перепроверенными живыми прогонами AC1/AC2/AC7 ниже | `test/wall-thickness.test.mjs`, тест «issue #288 keeps a shared wall attached beyond a short node ray finite» — содержимое идентично тому, что описано в CODE-REVIEW-288-r1.md |
Дополнительное процессное наблюдение (не находка в скоупе #288, не блокирует):
вердикт-комментарии в issue (ни r1, ни спек-раунды) не называют SHA явно —
его пришлось восстанавливать по телу committed review-документов и по
проверке существования объектов в репозитории. Именно это в данном раунде
и обнажило факт ребейза. Стоит на будущее указывать SHA прямо в
комментарии-вердикте, а не только в файле документа.
## Унаследовано из r0 (без повторной проверки)
Из-за полного пересмотра (см. выше) в этом раунде почти ничего не наследуется
слепо — весь код, тесты, гейты и смоки перепрогнаны заново на текущем HEAD.
Единственное, что не проверялось повторно и принимается как есть:
- Продуктовая рамка и соответствие J1/J6 из `docs/SCOPE.md`, зафиксированные
в аналитике issue (комментарий 0) и в SPEC-REVIEW-288-r1/r2 (зелёный на
спеке) — текст спеки в этом диапазоне не менялся относительно принятой
версии r2 спеки (docs/specs/288-bounded-multiwall-corridor.md идентичен
версии, получившей зелёный вердикт SPEC-REVIEW-288-r2).
- Диагноз причины (узел `577,299`, лучи `349/120/5` шагов, `30/30/30` см,
соседняя стена `20` см, радиус `4×15=60`, вырез `60−15=45`) — подтверждён
владельцем в теле issue, не переоценивался повторно.
## Как проверялось
### Прочитанное
`docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §2.7/§2.9/§4/§7/§8/§12, тело issue
#288 и все 6 комментариев (аналитика, «ТЗ готово», оба вердикта спек-ревью,
правки автора, вердикт CODE-REVIEW-288-r1), `docs/specs/
288-bounded-multiwall-corridor.md` целиком, `docs/WALL-THICKNESS.md`/
`docs/ARCHITECTURE.md`/`docs/TESTING.md` до и после диффа, полный diff
`src/wall-thickness.ts`, `test/wall-thickness.test.mjs`,
`scripts/mutation-gate.mjs`, `demo/smoke_real_plan_masonry.mjs`.
### Гейты — выполнено (живые прогоны на текущем HEAD)
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто |
| Unit | `npm test` | `1224 tests, 1223 pass, 1 skipped, 0 fail` |
| Build + bundle parity | `npm run build`, `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | идентичны; `npm run bundle:sync` синхронизировал третью копию (`demo/srv/assets`) без расхождений |
| Docs fingerprint | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` — подтверждает, что `sourceFingerprint`/`imageSha256` в `screenshots.json` соответствуют текущему `src/**` и текущим PNG байт-в-байт |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 3 «зарегистрированные связи» на `MultiWallNodeRaySupport`: `smoke_junction_patch_resilience`, `smoke_multiwall_junction`, `smoke_multiwall_strip_containment`; порог «широкого» символа (36) не превышен |
| Целевой смок AC1/AC2/AC5 | `node demo/smoke_real_plan_masonry.mjs` (после `bundle:sync`) | `second-floor: gapCount 0, totalGapSteps 0` (45757 samples); `first-floor: gapCount 0, totalGapSteps 0` (8048 samples, 830 skipped); все 8 проверок `true`, итог `OK` |
| 3 «зарегистрированные связи» | `node demo/smoke_junction_patch_resilience.mjs`, `node demo/smoke_multiwall_junction.mjs`, `node demo/smoke_multiwall_strip_containment.mjs` | все три — `OK`, все булевы поля `true` (включая `excessWedgeIsEmpty` #249, `nodeRemainsFilled` #261, `viewStopsAtFiniteRayEndpoint` #271, `cell_5_mixed_depth_t_*`/`cell_1_thick_crossbar_t_*` #275) |
| Инварианты модели | `npm run invariants -- --config test/fixtures/real-plan-second-floor.json` и то же для `real-plan-first-floor.json` | оба: `Инварианты выполнены: ссылки разрешимы, записи толщины находятся` |
| Мутант AC7 | `node scripts/mutation-gate.mjs --id=multi-wall-shared-continuation-protection-disabled` | чистый прогон зелёный, мутант красный (`тест покраснел, как обязан`), `поймано 1 из 1` |
| Реестр мутантов целиком | `node scripts/mutation-gate.mjs --check` | все патчи (включая новые из `#260` и `#288` в одном файле) применяются к текущему коду без конфликтов, `exit 0` |
| **Полная золотая матрица** | `npm run golden:verify` | все сценарии `passed`, включая `wall-junctions-*`, `junction-patch-resilience-*`, `wall-union-isolation-*`, `multiwall-junction-bevel-view-dark`, `orthogonal-strip-cell-5/1-view-dark`, `wall-key-roundtrip-view-dark`, `isometric-wall-junctions-dark`; `exit 0` |
Полная золотая матрица прогнана, а не пропущена как «предрелизный гейт»: AC5/6
прямо требуют согласованности всех consumers и запрещают тихое повышение
tolerance, а сам фикс меняет общий для всех junction-сценариев
`bevelMultiWallBody`, так что дешевле было прогнать полный матрикс (инструмент
не даёт частичного прогона), чем полагаться только на semantic-smoke.
### Гейты — не выполнено, с причиной
- `python -m pytest tests_backend -q` — диапазон не трогает
`custom_components/**/*.py` (только сгенерированный бандл).
- `npm run performance`/`benchmark_*` — не назван в AC8; спека §6 относит
performance-профиль к предрелизному гейту («перед beta»).
### Прочитано, не исполнено
- Побайтовое содержимое пересобранного `dist/houseplan-card.js` не читал
построчно — детерминированная сборка и `cmp` трёх копий достаточны.
- Логика `nearbyBuckets`/дедупликации `continuationKeys`/самоисключения через
`candidate.other == node.point` в `buildMultiWallNodeMap`
(`src/wall-thickness.ts:1704-1743`) прочитана построчно и прослежена вручную
на топологии AC3 (`349/120/5` шагов, `30/30/20` см) — корректна, совпадает с
тем, что уже отследил r1.
- Мутант `multi-wall-shared-continuation-protection-disabled` инвертирует
ровно одно условие (`candidate.kind !== 'shared'` → `=== 'shared'`) — при
такой инверсии первым делом принимаются кандидаты `kind !== 'shared'`
(например, `outer`), что напрямую противоречит утверждению теста AC3 про
`outerNode` (`continuations.length === 0` для внешней стены) и одновременно
отключает восстановление настоящей общей стены — проверено чтением патча,
подтверждено живым прогоном (мутант красный).
## Находки
Ни одной находки уровня High или Medium в скоупе задачи. Единственная
непогашенная Low-находка — унаследованная и не изменившаяся с r1 (см. таблицу
закрытия выше), автор может закрыть её опционально; не блокирует зелёный
вердикт при отсутствии High.
## Что проверено и корректно
- **AC1/AC2** — доказаны напрямую живым прогоном: оба реальных плана дают
`gapCount: 0`/`totalGapSteps: 0`; проверены все точки контура (плотная
выборка 45757/8048), не только 4 исторических гэпа; `PLANS`/
`KNOWN_GAP_COUNT`/`KNOWN_GAP_STEPS` — нули в том же коммите (`888a7eae`), а
не в отдельном; `npm run invariants` чист на обоих планах.
- **AC3** — юнит проверяет три масштаба (`cell_cm: 1/5/30`), прямой и
реверсированный порядок rays/endpoints; отдельно проверяет, что при
`kind: 'outer'` continuation не регистрируется (не нарушает контракт #249).
Ограничение теста (метаданные, не геометрия тела) не устраняет доказательную
силу: geometрический результат независимо подтверждён смоком AC1/AC2 и
мутантом AC7.
- **AC4** — все семь прежних контрактов (#249/#261/#271/#272/#275/#278/#279)
зелёные и в `npm test`, и в трёх браузерных смоках с прямым совпадением по
`MultiWallNodeRaySupport`.
- **AC5** — та же продукционная сборка (`bundle:sync`) и `isPointInFill` в
Plan/View; структурные консюмеры используют один и тот же canonical
`wallBodiesGeometry` (`multiWallContinuationStripGeometry` вызывается один
раз внутри `bevelMultiWallBody`, до всех downstream потребителей — проверено
чтением). Полная золотая матрица (см. выше) подтверждает согласованность
всех сцен, включая Static/hidden Iso и light/sun.
- **AC6** — семантический `gapCount === 0` подтверждён живым прогоном
(первичное доказательство); полный `golden:verify` дополнительно подтвердил
отсутствие визуальной регрессии без единой переприёмки baseline.
- **AC7** — мутант зарегистрирован, инвертирует ключевую проверку, убивается
юнитом AC3 — воспроизведено живым прогоном в этом раунде.
- **AC8** — все локальные гейты выполнены и зелёные (таблица выше).
- **Порядок слияния из issue/§10** — теперь фактически соблюдён (см.
«Закрытие раунда r1»), а не только продекларирован.
- **Docs/changelog** — `docs/WALL-THICKNESS.md`/`ARCHITECTURE.md`/
`TESTING.md` описывают именно то, что делает код (построчно сверено с
диффом `src/wall-thickness.ts`); `docs/CHANGELOG.md`/`CHANGELOG.ru.md`
правлены в том же коммите `888a7eae`, `User-Visible: yes`. Трейлеры
`Issue: #288` присутствуют на всех 7 коммитах диапазона; `User-Visible: yes`
— ровно на fix-коммите, `no` — на остальных (спека, документы ревью,
пересъёмка скриншотов).
- **Одно число — один источник.** Дифф не вводит и не меняет ни одной
пользовательски видимой числовой величины — это фикс геометрии
заливки/рендера стен, не текстовое отображение размеров;
`test/single-source-numbers.test.mjs` прошёл в составе `npm test` без
затрагивающих его изменений.
- **Совместимость/touch/security/perf** — проверено чтением: изменение
локально по `rays`/`supports` одного узла, не вводит новый глобальный проход
по стенам, не меняет schema/config/layout/Optimize (диапазон не трогает эти
файлы), не добавляет HA service calls.
## Гейты, которые прогнал/не прогнал — сводка
Прогнал: `tsc --noEmit`, `npm test`, `npm run build` + `cmp` трёх копий
бандла (`npm run bundle:sync`), `node scripts/check-docs.mjs`,
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`,
`demo/smoke_real_plan_masonry.mjs`, три «зарегистрированные связи» смока,
`npm run invariants` на конфигах обеих real-plan фикстур, целевой мутант AC7,
`node scripts/mutation-gate.mjs --check` (весь реестр), **полный**
`npm run golden:verify`.
Не прогнал: `pytest tests_backend` (Python не тронут), performance-профили
(не названы в AC8).
## Вердикт
Зелёный. Единственная Medium-находка r1 (нарушенный порядок слияния #260 →
пересъёмка скриншотов) закрыта фактически, не декларативно: `#260` слит в
`dev`, ветка `#288` перестроена на новый `dev`, скриншоты пересняты после
слияния и подтверждены чистым `check-docs`. Полный повторный прогон всех
гейтов, включая полную золотую матрицу, не выявил новых находок уровня
Medium/High. Остаётся одна открытая Low-находка (AC3-тест ограничен
метаданными) — не блокирует, оставлена на решение автора, как и в r1.