docs: review document for #288
Validate / docs (push) Failing after 39s
Validate / process-workflow-sync (push) Successful in 38s
Validate / provenance (push) Successful in 43s
Validate / process-gate (push) Failing after 44s
Validate / changes (push) Successful in 29s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / reuse (push) Successful in 1m0s
Validate / smoke (1) (push) Skipped
Validate / smoke (2) (push) Skipped
Validate / smoke (3) (push) Skipped
Validate / smoke_done (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped

Issue: #288
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-24 13:14:55 +00:00
parent f2c0721a9d
commit 10fa0dc7fb
+277
View File
@@ -0,0 +1,277 @@
# CODE-REVIEW-288-r3
- **Issue:** https://github.com/Matysh/houseplan-card/issues/288
- **Этап:** code (PROCESS.md §2.7)
- **Заход:** r3 · блокирующих циклов израсходовано 1 из 4
- **Ветка:** `issue/288-bounded-multiwall-corridor`
- **HEAD на момент ревью:** `f2c0721a9da6db255145abd14f1b52c20e15bbad`
- **Диапазон:** `origin/dev...HEAD` (`origin/dev` = `f472af88a3e9c89fec1f2b90b6f2c10471d5c693`,
включает слитый в `dev` #289)
## Почему разбор полный, а не по дельте
Формально третий заход код-ревью, но объём разбора — полный §2.7, а не по
дельте §2.10. Причина явно названа владельцем в issue, а не выведена мной:
после зелёного вердикта CODE-REVIEW-288-r2 слияние ветки в `dev` конфликтовало
(владелец, issue #288, предпоследний комментарий): *«Не удалось только
слияние: ветка `issue/288-bounded-multiwall-corridor` конфликтует с `dev`. …
Повторный прогон ревью — не формальность: после ребейза на новый `dev` это
другой код, и принимать его без проверки нельзя»*. Это прямое применение
PROCESS.md §7.2/§2.10 («ребейз на ушедший вперёд dev — другой код»),
подтверждённое фактом: `git merge-base origin/dev HEAD` = `f472af88` —
текущий tip `dev`, то есть ветка `#288` целиком лежит поверх свежего `dev`, в
который между r2 и r3 влился #289 («fix: prevent mixed-role walls during
resize»).
Доказательство, что это настоящий ребейз, а не просто новый коммит сверху:
SHA r2 (`HEAD на момент ревью: 162f9b0eca8f61b4ae1d716785f1f94f8d448acc`) не
существует в текущем репозитории:
```
$ git cat-file -e 162f9b0eca8f61b4ae1d716785f1f94f8d448acc
fatal: Not a valid object name 162f9b0eca8f61b4ae1d716785f1f94f8d448acc
```
Коммит с тем же сообщением («docs: recapture screenshots after fixture
repair») присутствует в текущей истории, но под новым хешем `406e46ec` — это
переигранный (rebased), а не идентичный объект: контент коммита совпадает,
хеш — нет, потому что родитель сменился (старый `dev` → новый `dev` с #289).
Итог: пункты 3 и 5 инструкции («чем закрыта находка» / «унаследовано»)
выполнены ниже, но объём проверки — полный §2.7, включая живые прогоны всех
гейтов заново на текущем HEAD, как и в r2.
## Скоуп диффа
`git diff origin/dev...HEAD --stat`: `src/wall-thickness.ts` (+134/-6, то же
ядро фикса, что в r1/r2), `test/wall-thickness.test.mjs` (+51),
`scripts/mutation-gate.mjs` (+14), `demo/smoke_real_plan_masonry.mjs` (числа
`PLANS`/`KNOWN_GAP_*` на нулях), `docs/{WALL-THICKNESS,ARCHITECTURE,TESTING,
CHANGELOG,CHANGELOG.ru}.md`, 2 PNG + `docs/images/screenshots.json`
(пересъёмка после интеграции с #289), `docs/reviews/{SPEC-REVIEW-288-r1,
SPEC-REVIEW-288-r2,CODE-REVIEW-288-r1,CODE-REVIEW-288-r2}.md` (артефакты
предыдущих раундов), `dist/houseplan-card.js` и
`custom_components/houseplan/frontend/houseplan-card.js` (сгенерированные,
класс D).
Продуктовое ядро **не изменилось ни байтом** относительно того, что получило
зелёный вердикт в r2. Прямая проверка контентом, а не по хешам коммитов:
```
$ git diff e8a4db79..HEAD --stat # e8a4db79 — коммит доки CODE-REVIEW-288-r2
docs/images/06-device-display-preview.png | Bin 327898 -> 327899 bytes
docs/images/07-background-editor.png | Bin 304173 -> 304173 bytes
docs/images/09-device-info.png | Bin 146160 -> 146170 bytes
docs/images/screenshots.json | 26 +++++++++++++-------------
4 files changed, 13 insertions(+), 13 deletions(-)
```
Единственная разница между деревом, которое видел r2, и текущим HEAD — это
одна дополнительная пересъёмка docs-скриншотов (`f2c0721a`, «после интеграции
с #289»), нужная потому, что #289 тоже трогал `docs/images/screenshots.json`
и правит фичи по соседним экранам (`06-device-display-preview`,
`07-background-editor`, `09-device-info` — не относятся к multi-wall junction,
относятся к общему `sourceFingerprint`, который считается по всему `src/**`).
`src/wall-thickness.ts`, `test/wall-thickness.test.mjs`,
`scripts/mutation-gate.mjs`, `demo/smoke_real_plan_masonry.mjs` идентичны
r2 побайтово.
Проверено также, что #289 не пересекается с продуктовым кодом #288: `#289`
трогает `src/resize.ts`, `src/i18n/*.json`, `docs/RESIZE.md`,
`demo/smoke_room_resize.mjs`, `demo/golden/baselines/safe-resize-*` — ни один
из этих путей не входит в диапазон `#288` и не пересекается с
`wall-thickness.ts`/`mutation-gate.mjs`/`wall-thickness.test.mjs`. Единственная
общая поверхность двух задач — общий отпечаток docs-скриншотов
(`screenshots.json`, весь `src/**`) и три копии бандла — механическое
пересечение, не логическое.
Напоминание ядра изменения (не менялось с r1): `buildMultiWallNodeMap`
добавляет каждому `MultiWallNodeRay` поле `continuations` — конечные общие
полосы («shared»-стены), примыкающие к дальнему концу одного из `supports`
луча. `bevelMultiWallBody` восстанавливает эти полосы
(`multiWallContinuationStripGeometry`) внутри маски узла и `union`-ит
результат вместо старой тройки `outside/preservedExterior/localInside`.
## Закрытие раунда r2
r2 — зелёный вердикт, High/Medium в этом раунде не было. Единственный пункт,
требующий явного отслеживания в r3, — процессный (не находка в скоупе #288,
как и в r2):
| Из r2 | Статус в r3 |
|---|---|
| Процессное наблюдение r2: вердикт-комментарии не называют SHA явно | Не устранено: комментарий владельца с решением «ребейзить и вернуть на code-review» также не называет SHA. SHA пришлось восстанавливать так же, как в r2 — проверкой существования объекта (`git cat-file -e 162f9b0e…` → отсутствует) и сверкой сообщений коммитов. Не блокирует и не заводится отдельным issue (уже отмечено дважды в r2 и здесь — не новая находка, а тот же неустранённый паттерн; решение по нему не в скоупе #288). |
Единственная оставшаяся содержательная находка — унаследованная Low из r1,
не тронутая ни в r2, ни в этом раунде (см. ниже).
## Унаследовано из r2 (без повторной проверки по существу, но перепроверено гейтами)
Формально разбор полный (rebase = «другой код», см. выше), поэтому все AC
перепроверены живыми прогонами на текущем HEAD (таблица гейтов ниже) — не
«наследование без проверки». Тем не менее по содержанию не пересматривались
повторно, так как дельта их не касается:
- Продуктовая рамка и соответствие J1/J6 из `docs/SCOPE.md`
(`docs/SCOPE.md:39-46`) — текст спеки не менялся с версии, принявшей
SPEC-REVIEW-288-r2 (зелёный); `docs/specs/288-bounded-multiwall-corridor.md`
идентичен варианту, разобранному в CODE-REVIEW-288-r1/r2.
- Диагноз причины (узел `577,299`, лучи `349/120/5` шагов, `30/30/30` см,
соседняя стена `20` см, радиус `4×15=60`, вырез `60−15=45`) — принят
владельцем в теле issue, переоценке в r1/r2/r3 не подвергался.
- Ручная экспериментальная проверка риска «маска узла A съедает то, что узел B
восстановил через `continuations`» (синтетические сценарии, документированы
в CODE-REVIEW-288-r1.md, раздел «Прочитано, не исполнено») — не повторялась,
код `multiWallContinuationStripGeometry`/`preservedExterior` не менялся.
Документы-источники: `docs/reviews/SPEC-REVIEW-288-r2.md` (зелёный, SHA
диапазона `fba615e9`), `docs/reviews/CODE-REVIEW-288-r1.md` (SHA `4aa79dee`,
не существует в текущем дереве — восстановлен по контенту),
`docs/reviews/CODE-REVIEW-288-r2.md` (SHA `162f9b0e`, также не существует —
восстановлен по контенту, см. выше).
## Как проверялось
### Прочитанное
`docs/SCOPE.md` (§ Core user jobs, J1/J6), `AGENTS.md`, `PROCESS.md`
§2.7/§2.9/§2.10/§4/§7/§8/§12, тело issue #288 и все 8 комментариев (аналитика,
«ТЗ готово», оба вердикта спек-ревью, правки автора, оба вердикта
код-ревью r1/r2, решение владельца о ребейзе), `docs/specs/
288-bounded-multiwall-corridor.md` целиком, `docs/WALL-THICKNESS.md`/
`docs/ARCHITECTURE.md`/`docs/TESTING.md` (диффы коммита `17086d3c`),
`docs/reviews/CODE-REVIEW-288-r1.md`, `docs/reviews/CODE-REVIEW-288-r2.md`,
полный diff `src/wall-thickness.ts`, `test/wall-thickness.test.mjs`,
`scripts/mutation-gate.mjs`, `demo/smoke_real_plan_masonry.mjs`, полный diff
#289 (`git diff 6a4e665d^..a7618151 --stat`) — для подтверждения отсутствия
файлового пересечения с #288.
### Гейты — выполнено (живые прогоны на текущем HEAD)
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто |
| Unit | `npm test` | `1225 tests, 1224 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)` — подтверждает, что пересъёмка `f2c0721a` соответствует текущему `src/**` |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 3 «зарегистрированные связи» на `MultiWallNodeRaySupport` (то же множество, что в r1/r2): `smoke_junction_patch_resilience`, `smoke_multiwall_junction`, `smoke_multiwall_strip_containment`; изменено файлов `src/**`: 1, символов на изменённых строках: 12; порог «широкого» символа (36 из 181 смоков) не превышен |
| Целевой смок 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/#289 в одном файле) применяются к текущему коду без конфликтов, `exit 0` |
| **Полная золотая матрица** | `npm run golden:verify` | 80/80 сценариев `passed`, 0 `failed`, включая `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`, а также golden-сцены #289 (`safe-resize-handles-clamp-*`); `exit 0` |
| Одно число — один источник | `node --test test/single-source-numbers.test.mjs` | pass |
Полная золотая матрица прогнана по тем же причинам, что в r2: AC5/AC6 прямо
требуют согласованности всех consumers, инструмент не даёт частичного
прогона, а сама пересъёмка скриншотов после интеграции с #289 — ровно тот
случай, когда дешевле подтвердить визуальную согласованность целиком, чем
полагаться только на 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`) не перечитывалась заново построчно в
этом раунде — файл байтово идентичен версии, разобранной построчно в r1/r2
(подтверждено `git diff e8a4db79..HEAD -- src/`, пустой вывод).
- Отсутствие файлового пересечения между #289 и #288 — проверено чтением
списков файлов (`comm`-эквивалент вручную): пересечение пустое, кроме
`docs/images/screenshots.json` и трёх копий бандла (механическое, не
логическое).
## Находки
Ни одной находки уровня High или Medium в скоупе задачи в этом раунде.
Единственная непогашенная **Low** — унаследованная из r1, не изменившаяся ни
в r2, ни в r3: `test/wall-thickness.test.mjs`, тест «issue #288 keeps a shared
wall attached beyond a short node ray finite» проверяет только метаданные
`ray.continuations`, не строит `wallBodiesGeometry` и не измеряет фактическую
геометрическую потерю площади. Не блокирует: geometрический результат
подтверждён напрямую живым прогоном AC1/AC2 (`gapCount: 0` на обоих реальных
планах) и мутантом AC7 в этом же раунде. Автор может закрыть опционально.
Процессное наблюдение (не находка в скоупе #288, не блокирует, повторяется
третий раз подряд — r2 уже фиксировал это же наблюдение): вердикт-комментарии
и комментарии с процессными решениями по этому issue не называют SHA явно.
## Что проверено и корректно
- **AC1/AC2** — доказаны напрямую живым прогоном: оба реальных плана дают
`gapCount: 0`/`totalGapSteps: 0` на текущем HEAD после ребейза; плотная
выборка (45757/8048 точек), не только 4 исторических гэпа; `npm run
invariants` чист на обоих планах.
- **AC3** — юнит проверяет три масштаба (`cell_cm: 1/5/30`), прямой и
реверсированный порядок rays/endpoints, отдельно проверяет отсутствие
continuation при `kind: 'outer'` (не нарушает #249). Ограничение теста
(метаданные, не геометрия тела) компенсируется смоком AC1/AC2 и мутантом
AC7 — как и в r1/r2 (см. Low выше).
- **AC4** — все семь прежних контрактов (#249/#261/#271/#272/#275/#278/#279)
зелёные в `npm test` и в трёх браузерных смоках с прямым совпадением по
`MultiWallNodeRaySupport`.
- **AC5** — единственный canonical `wallBodiesGeometry`
(`multiWallContinuationStripGeometry` вызывается один раз внутри
`bevelMultiWallBody`, до всех downstream потребителей — проверено чтением,
файл не менялся). Полная золотая матрица подтверждает согласованность всех
сцен, включая Static/hidden Iso и light/sun, на текущем HEAD.
- **AC6** — семантический `gapCount === 0` подтверждён живым прогоном на
текущем HEAD; полный `golden:verify` (80/80 passed) подтверждает отсутствие
визуальной регрессии без единой переприёмки baseline специально для #288.
- **AC7** — мутант зарегистрирован, инвертирует ключевую проверку, убивается
юнитом AC3 — воспроизведено живым прогоном на текущем HEAD.
- **AC8** — все локальные гейты выполнены и зелёные (таблица выше).
- **Порядок слияния из issue/§10 (наследие r1)** — остаётся соблюдённым:
`#260` уже в `dev` с r2, и не затрагивается этим раундом.
- **Docs/changelog** — `docs/WALL-THICKNESS.md`/`ARCHITECTURE.md`/
`TESTING.md` описывают именно то, что делает код (файлы не менялись с r2,
сверка построчно проведена в r1/r2 и не требует повтора при неизменном
диффе); `docs/CHANGELOG.md`/`CHANGELOG.ru.md` правлены в fix-коммите
`17086d3c`, `User-Visible: yes`. Трейлеры `Issue: #288` — на всех 9 коммитах
диапазона; `User-Visible: yes` — ровно на fix-коммите `17086d3c`, `no` — на
остальных (спека, документы ревью, пересъёмки скриншотов).
- **Одно число — один источник.** Дифф не вводит и не меняет ни одной
пользовательски видимой числовой величины; `test/single-source-numbers.test.mjs`
прошёл отдельным прогоном на текущем HEAD.
- **Совместимость/touch/security/perf** — диапазон не трогает
`schema`/`config`/`i18n`-файлы (проверено: `git diff origin/dev...HEAD
--name-only | grep -E "schema|config|i18n"` → пусто); изменение локально по
`rays`/`supports` одного узла, не вводит новый глобальный проход по стенам.
- **Независимость от #289** — файловые множества диффов #288 и #289 не
пересекаются в продуктовом/тестовом коде (проверено чтением списков путей);
общая поверхность — только `docs/images/screenshots.json` и три копии
бандла, что и объясняет коммит `f2c0721a`.
## Гейты, которые прогнал/не прогнал — сводка
Прогнал: `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` (80/80 passed), `test/single-source-numbers.test.mjs`.
Не прогнал: `pytest tests_backend` (Python не тронут), performance-профили
(не названы в AC8).
## Вердикт
Зелёный. Ребейз на ушедший вперёд `dev` (после интеграции #289) не изменил
ни строки продуктового или тестового кода #288 — подтверждено прямым
`git diff` содержимого дерева, а не только пересчётом хешей. Единственное
новое в этом диапазоне — повторная пересъёмка docs-скриншотов, подтверждённая
чистым `check-docs`. Полный повторный прогон всех гейтов, включая полную
золотую матрицу (80/80), не выявил новых находок уровня Medium/High.
Остаётся одна унаследованная Low-находка (AC3-тест ограничен метаданными,
не блокирует) и одно неустранённое процессное наблюдение (SHA не называется
в вердикт-комментариях) — оба не блокируют зелёный вердикт.