diff --git a/docs/reviews/CODE-REVIEW-288-r2.md b/docs/reviews/CODE-REVIEW-288-r2.md new file mode 100644 index 00000000..67b42d1d --- /dev/null +++ b/docs/reviews/CODE-REVIEW-288-r2.md @@ -0,0 +1,225 @@ +# 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.