Files
houseplan-card/docs/reviews/CODE-REVIEW-258-r1.md
T
2026-08-23 11:38:37 +00:00

205 lines
23 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-258-r1
- Issue: [#258](https://github.com/Matysh/houseplan-card/issues/258) — «На 1.67.0-beta.4 после «Оптимизировать» появились белые клинья в местах схода стен»
- Ветка `issue/258-wall-key-storage-roundtrip`, коммит `28eaf86662819651b2c75e7423280f10820a757d`
- Этап: code (PROCESS.md §2.7)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Вердикт: **жёлтый**
## Скоуп
ТЗ `docs/specs/258-wall-key-storage-roundtrip.md` описывает регресс: `wallKey()`
квантует середину стены через `Math.round`, и у стены нечётной длины в шагах
решётки середина попадает точно на границу округления. Точное узловое
представление вершины (`83/240`) и её девятизнаковое persisted-представление
(#224) дают два разных `key` для одного и того же ребра; `lookupWall()`
находит запись только по точному совпадению строки или терпимому запасу
ровно в полшага, а `thicknessCmAt()` — ещё и по точным `a/b` через
`exactCoveringWall()`. Реализация стабилизирует генерацию `key` near-grid
нормализацией endpoint'ов, добавляет строгий same-span lookup по точным `a/b`
между exact-key и legacy midpoint fallback, и пересобирает диагностику
`checkWallKeys` в `scripts/model-invariants.mjs`.
**Важное уточнение хронологии, которое меняет оценку скоупа.** Между
утверждением ТЗ (spec-review, зелёный, `docs/reviews/SPEC-REVIEW-258-r1.md`)
и завершением реализации владелец сам проверил исходный диагноз исполнением
на своих реальных экспортах и
[отозвал его](https://github.com/Matysh/houseplan-card/issues/258#issuecomment-5385705030):
переписанные Optimize ключи не меняют `wallIntervals`/`wallEdgeBodies`/
`buildMultiWallNodeMap` — тела стен и карта узлов побайтово идентичны
независимо от того, какой из двух ключей записан. Реальная причина белых
клиньев на скриншотах владельца осталась неизвестной; названы два других
следа (рост `wall-thickness.ts` на 760 строк в beta.4 от #249/#253, и
сплошное ребро без записи толщины, существовавшее уже на beta.3). Реализация
всё равно доведена по первоначальному контракту ТЗ — это осмысленное решение
(правит реальный, отдельно доказанный дефект класса «одно ребро — разные
ответы у потребителей»), но релиз-артефакты этой задачи заявляют, что именно
зарепорченный симптом устранён. Разбор ниже отдельно оценивает код (полностью
корректен по контракту ТЗ) и это продуктовое несоответствие.
## Как проверялось
1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §2.7/§2.10/§7–§8/§12,
`docs/WALL-THICKNESS.md`, `docs/CONFIG-COMPATIBILITY.md`,
`docs/ARCHITECTURE.md` (участок Room Resize/wall key), тело issue #258 и
**все** комментарии, включая отзыв диагноза и хендофф от 2026-08-23, и
ТЗ/спек-ревью документ.
2. Первый заход код-ревью — разбор полный, без «дельты» (§2.10 неприменим).
3. `git diff origin/dev...HEAD --stat` — 25 файлов; продуктовый код —
только `src/wall-thickness.ts` (+40/-…). Остальное: тесты, `scripts/**`
(class B), `docs/**` (class C), `dist/**` +
`custom_components/houseplan/frontend/**` (class D, сгенерированы).
4. Прочитан весь diff `src/wall-thickness.ts`: `keyEpsilon()`,
`canonicalKeyCoordinate()`, изменённый `wallKey()`, изменённый
`lookupWall()` (новый same-span шаг между exact-key и legacy fallback).
Отдельно проверено, что `thicknessCmAt()` (:319) и `cmsForPoly()` (:1366)
оба вызывают именно этот `lookupWall()` — то есть AC2/AC3 «один resolved
wall entry для всех структурных потребителей» действительно следует из
единой точки входа, а не из совпадения двух отдельных формул.
5. Прочитан diff `scripts/model-invariants.mjs`, `scripts/mutation-gate.mjs`,
`scripts/smoke-links.mjs`, все новые/изменённые тесты
(`test/wall-thickness.test.mjs`, `test/plan-optimizer.test.mjs`,
`test/model-invariants.test.mjs`, `test/golden-matrix.test.mjs`,
`demo/golden/harness.mjs`, `demo/golden/matrix.mjs`,
`demo/smoke_wall_key_roundtrip.mjs`), документация
(`docs/WALL-THICKNESS.md`, `docs/ARCHITECTURE.md`,
`docs/CONFIG-COMPATIBILITY.md`, `docs/TESTING.md`, оба changelog).
6. Прогнаны гейты (таблица ниже) и три новых/изменённых mutation-анкера
отдельно, чтобы убедиться, что связанные с ними тесты умеют падать.
### Гейты
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный |
| Unit | `npm test` | 1159 passed, 0 failed, 0 skipped |
| Build + bundle parity | `npm run build && npm run bundle:sync` + `cmp` трёх копий | `dist/houseplan-card.js` = `custom_components/houseplan/frontend/houseplan-card.js` = `demo/srv/assets/houseplan-card.js` |
| Docs fingerprint | `node scripts/check-docs.mjs` | «Documentation checks passed (7 files, 10 external links)» |
| Smoke selection | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 173 смоков в матрице; 2 «зарегистрированные связи»: `smoke_resize_wall_thickness.mjs` (← `closePoint`, `wallDir`), `smoke_wall_key_roundtrip.mjs` (← `canonicalKeyCoordinate`, `keyEpsilon`) — обе прогнаны |
| Целевые смоки | `node demo/smoke_wall_key_roundtrip.mjs`, `node demo/smoke_resize_wall_thickness.mjs` | оба `OK`, все под-проверки `true` |
| Model invariants | часть `npm test` (`test/model-invariants.test.mjs`, включает parity-тест копии `wallKey` и фикстур-sweep) | зелёный; отдельный `npm run invariants -- --config <файл>` не запускался — diff не даёт нового полного экспорта конфигурации сверх уже покрытых unit-фикстур, а `npm test` уже гоняет полную проверку по всем моделям проекта (демо-фикстуры + demo-стенд) |
| Mutation-анкеры (новые/изменённые) | `node scripts/mutation-gate.mjs --id=wall-key-storage-normalization-disabled` / `--id=wall-exact-span-fallback-disabled` / `--id=invariant-wall-key-storage-normalization-disabled` / `--id=invariant-keys-cry-wolf` / `--id=invariant-keys-hide-stale-observation` | все 5: «тест покраснел, как обязан» — тесты действительно умеют падать |
| Golden | часть `npm test` (`test/golden-matrix.test.mjs`); `npm run golden:verify` (браузерный рендер) не запускался | сценарий `wall-key-roundtrip-view-dark` зарегистрирован и покрыт unit-проверкой контракта; baseline не принимался (правильно — только через полный Linux CI artifact) |
| Backend | — | не запускался: diff не трогает `custom_components/**/*.py` |
| Performance | — | не запускался: AC не называет перф-эффект, спецификация явно фиксирует `O(1)`/`O(W)` без изменений |
**Не прогнано и почему:** полный browser-`golden:verify` и полный набор из 173
смоков — задача касается одного узкого T-стыка и явно выбранных потребителей;
`smoke-select` назвал ровно два релевантных смока, оба прогнаны, остальные 171
не относятся к диффу и полный прогон — предрелизный гейт, не гейт ревью.
`pytest tests_backend` — diff не трогает Python. Отдельный `npm run invariants
-- --config <файл>` на внешнем экспорте — не требовался: diff не добавляет
новую геометрию, которую не покрыли бы уже прогнанные unit-фикстуры и полный
sweep внутри `npm test`.
## Проверка AC
| AC | Статус | Как доказано |
|---|---|---|
| AC1 (один key для exact/9-знака, odd/even, ±, reversed) | Выполнен | `test/wall-thickness.test.mjs` «issue 258 wallKey survives...» — 4 кейса + негативный тест на epsilon-границу; mutant `wall-key-storage-normalization-disabled` убивается |
| AC2 (строгий same-span lookup, без parent/child/neighbour/parallel) | Выполнен | `test/wall-thickness.test.mjs` «issue 258 exact-span lookup...» — оба варианта ключа находят 29 см, явные негативные кейсы (`broken-parent/child/neighbour/parallel`) не принимаются; render-space (`coordScale`) тоже проверен; mutant `wall-exact-span-fallback-disabled` убивается |
| AC3 (Optimize канонизирует key, preview не пишет, идемпотентность после roundtrip) | Выполнен | `test/plan-optimizer.test.mjs` «issue 258 Optimize canonicalizes...» — deepEqual на preview, `changed:true`/`canonicalized:1` на первом Apply, `changed:false` на in-memory и backend-echo повторе. Код в `plan-optimizer.ts` не менялся: `rekeyWallsAfterMove()`/canonicalизация уже вызывают исправленный `wallKey()` — прочитано и подтверждено, отдельная вторая формула не добавлена |
| AC4 (инвариант сообщает оба mismatch, парность с production `wallKey`) | Выполнен, но **изменена формулировка относительно ТЗ §8 — явно заявлено автором** | `checkWallKeys()` больше не возвращает `violations` (массив всегда пуст) — несовпадение репортится только как `notes`/observation. Автор прямо попросил ревьюера отдельно оценить это отступление. Технически обоснованно: после фикса `lookupWall()` резолвит любую запись с валидными `a/b` независимо от старого key через same-span шаг, поэтому «нарушение» никогда не воспроизводимо для записи с координатами — а `checkWallKeys` как «hard violation» немедленно покрасил бы `demo/fixtures/large-house.mjs`/`visual-matrix.mjs`, чьи ключи не по контракту по другой причине (заведено отдельно, #260). Даунгрейд подтверждён тестом «#258: старый и неразбираемый compatibility key — наблюдение» и двумя mutation-анкерами (`invariant-keys-cry-wolf`, `invariant-keys-hide-stale-observation`), оба убиваются. Принимаю как согласованное с текущим `dev` решение, а не как найденную находку |
| AC5 (T-node без клина во всех потребителях до/после Optimize/reload) | Выполнен для минимизированного T-fixture из issue | `demo/smoke_wall_key_roundtrip.mjs` — прогнан, `OK`, 24 под-проверки `true` (Plan/View/kiosk/Static/hidden-Iso/clean-floor/light-barrier × canonical/affected); unit `test/wall-thickness.test.mjs` «issue 258 repaired span reaches intervals, junction nodes and masonry» — реальная геометрическая проба `assertProbeInside` |
| AC6 (golden light/dark T-node) | Выполнен по прецеденту проекта | Сценарий `wall-key-roundtrip-view-dark` добавлен и покрыт `test/golden-matrix.test.mjs`; только dark-вариант — совпадает с прецедентом трёх похожих T-стыковых сценариев (#249/#253: `wall-junctions-view-dark`, `junction-patch-resilience-view-dark`, `multiwall-junction-bevel-view-dark`), которые тоже dark-only; baseline не принимался локально (правильно) |
| AC7 (регрессии: legacy key-only, parent inheritance, #253, #249, #248, openings, two-ray) | Выполнен | Полный `npm test` зелёный (1159/1159), включая все именованные регрессионные наборы; новых провалов не внесено |
| AC8 (implementation loop + provenance) | Выполнен | `typecheck`/`test`/`build`/parity/`check-docs` — все зелёные (см. таблицу гейтов); коммит несёт `Issue: #258` / `User-Visible: yes`; оба changelog правлены в том же коммите |
## Находки
### Medium (в скоупе задачи — правится в ней же)
**Release-артефакты заявляют больше, чем доказано, и противоречат собственному
расследованию владельца в этом же issue.**
- Файл: `docs/CHANGELOG.md` (и `docs/CHANGELOG.ru.md`)
- Итог: «Thick-wall T-junctions no longer develop white wedges after
“Optimize plans”» / «После «Оптимизировать планы» в T-образных стыках
толстых стен больше не появляются белые клинья» — безусловное утверждение,
что именно зарепорченный в #258 симптом устранён.
- Сценарий, в котором это ломается: владелец обновляется на бету с этим
коммитом, открывает свой реальный план (тот самый, со скриншотов), нажимает
«Оптимизировать» — и, по его же собственному измерению
([комментарий](https://github.com/Matysh/houseplan-card/issues/258#issuecomment-5385705030)),
`wallEdgeBodies`/карта узлов для его двух реальных пространств не меняются
ни на бит между старым и новым кодом ключа. Если клинья на его скриншотах
вызваны не этим (а иным следом — код #249/#253 или ребро без толщины,
подробности в новом [#261](https://github.com/Matysh/houseplan-card/issues/261)),
changelog утверждает то, что не подтверждено, и создаёт основание закрыть
#258 без бета-регрессии, хотя визуальный дефект, из-за которого issue был
заведён, может остаться на месте.
- Почему это в скоупе и не High: код и все AC1–AC8 корректны и полностью
доказаны — правки не требуют переписывания реализации, только формулировки
release-артефактов (§13 ТЗ прямо включает их в DoD задачи). Достаточно
переформулировать записи `CHANGELOG.md`/`.ru.md` так, чтобы они описывали
фактически исправленный дефект («несогласованность потребителей толщины
стены при устаревшем/пересчитанном compatibility-key после Optimize»), а не
безусловно закрывали зарепорченный визуальный симптом. Тот же нюанс стоит
отразить и в финальном комментарии/закрытии #258 — новый
[#261](https://github.com/Matysh/houseplan-card/issues/261) уже открыт для
отдельного расследования реальной причины.
### Low (снято с записью)
- `scripts/model-invariants.mjs`, `titles` внутри `report()`: ключ `wall_key:
'Записи толщины, которые не найдутся по ключу'` теперь мёртв — `wall_key`
как `kind` violation больше никогда не push'ится (см. AC4 выше, `violations`
для этого инварианта теперь всегда `[]`). Не влияет на поведение — вывод
просто никогда не использует эту строку. Не блокирую: тривиальная
косметика, безопасно оставить или почистить в следующей правке того же
файла.
## Что проверено и корректно
- Единая точка генерации key (`wallKey`) и единая точка резолва (`lookupWall`)
— оба структурных потребителя (`thicknessCmAt`, `cmsForPoly`) проходят
через один и тот же исправленный `lookupWall()`; вторая формула нигде не
завелась.
- Near-grid нормализация консервативна: epsilon строго на порядки меньше
половины шага решётки, легитимная off-grid геометрия не снапится
(проверено тестом и mutant'ом).
- Same-span резолв — действительно same-span, не containment: negative-кейсы
на parent/child/neighbour/parallel явно протестированы и не проходят.
- Optimize канонизирует ключ через уже существующий код (`rekeyWallsAfterMove`
импортирует и использует исправленный `wallKey`), отдельной второй формулы
оптимизатора не появилось; preview не мутирует, повторный вызов и
backend-echo — no-op.
- Копия формулы в `scripts/model-invariants.mjs` прикреплена parity-тестом к
продуктовой `wallKey` (как и предписывало ТЗ и практика #233/#234) —
расхождение будет обнаружено автоматически.
- Три новых и два переработанных mutation-анкера действительно убивают
соответствующие правки (проверено запуском каждого).
- Bundle parity (`dist` = `custom_components/.../frontend` = `demo/srv/assets`)
и docs-fingerprint — в порядке; трейлеры коммита корректны, оба changelog
правились в одном коммите с кодом.
- AC7 (регрессии #248/#249/#253/legacy key-only/two-ray/openings) не сломаны —
полный `npm test` зелёный без единого нового провала.
## Чего не проверял
- Полный `npm run golden:verify` (браузерный рендеринг golden) и полный набор
173 смоков — не требуется объёмом задачи; ограничился двумя смоками,
названными `smoke-select`, плюс уже пройденной unit-частью golden-контракта.
- `python -m pytest tests_backend` — diff не касается `custom_components/**/*.py`.
- Perf-профили — AC не называет влияние на перф, а спецификация фиксирует
неизменную асимптотику; не проверял исполнением, принял по чтению кода
(линейный `lookupWall`, ни одного нового прохода по комнатам).
- Реальную причину белых клиньев на исходных скриншотах владельца — это вне
моей роли ревьюера кода этой задачи; зафиксировано находкой выше и новым
[#261](https://github.com/Matysh/houseplan-card/issues/261).
## Итог
Код и тесты для заявленного в ТЗ контракта (стабильный `wallKey`, строгий
same-span `lookupWall`, канонизация через Optimize, пересмотренная
диагностика инварианта) — корректны, полны и доказаны исполнением, включая
работающие mutation-анкеры. Единственная находка — Medium, в скоупе,
касается не кода, а точности формулировки release-артефактов, которые
безусловно заявляют устранение зарепорченного в #258 визуального симптома,
хотя собственное расследование владельца в этом же issue показывает, что
исправленный дефект не объясняет этот симптом на его реальных данных. Это
ровно случай «AC выполнены, но изменение не решает заявленный сценарий» —
жёлтый вердикт, возврат автору для правки формулировок в `docs/CHANGELOG.md`
и `docs/CHANGELOG.ru.md` (и, по желанию автора, уточнения в самом issue перед
закрытием).