23 KiB
CODE-REVIEW-258-r1
- Issue: #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)
и завершением реализации владелец сам проверил исходный диагноз исполнением
на своих реальных экспортах и
отозвал его:
переписанные Optimize ключи не меняют wallIntervals/wallEdgeBodies/
buildMultiWallNodeMap — тела стен и карта узлов побайтово идентичны
независимо от того, какой из двух ключей записан. Реальная причина белых
клиньев на скриншотах владельца осталась неизвестной; названы два других
следа (рост wall-thickness.ts на 760 строк в beta.4 от #249/#253, и
сплошное ребро без записи толщины, существовавшее уже на beta.3). Реализация
всё равно доведена по первоначальному контракту ТЗ — это осмысленное решение
(правит реальный, отдельно доказанный дефект класса «одно ребро — разные
ответы у потребителей»), но релиз-артефакты этой задачи заявляют, что именно
зарепорченный симптом устранён. Разбор ниже отдельно оценивает код (полностью
корректен по контракту ТЗ) и это продуктовое несоответствие.
Как проверялось
- Прочитаны
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.10 неприменим).
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, сгенерированы).- Прочитан весь 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 для всех структурных потребителей» действительно следует из единой точки входа, а не из совпадения двух отдельных формул. - Прочитан 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). - Прогнаны гейты (таблица ниже) и три новых/изменённых 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 симптом устранён.
- Сценарий, в котором это ломается: владелец обновляется на бету с этим
коммитом, открывает свой реальный план (тот самый, со скриншотов), нажимает
«Оптимизировать» — и, по его же собственному измерению
(комментарий),
wallEdgeBodies/карта узлов для его двух реальных пространств не меняются ни на бит между старым и новым кодом ключа. Если клинья на его скриншотах вызваны не этим (а иным следом — код #249/#253 или ребро без толщины, подробности в новом #261), changelog утверждает то, что не подтверждено, и создаёт основание закрыть #258 без бета-регрессии, хотя визуальный дефект, из-за которого issue был заведён, может остаться на месте. - Почему это в скоупе и не High: код и все AC1–AC8 корректны и полностью
доказаны — правки не требуют переписывания реализации, только формулировки
release-артефактов (§13 ТЗ прямо включает их в DoD задачи). Достаточно
переформулировать записи
CHANGELOG.md/.ru.mdтак, чтобы они описывали фактически исправленный дефект («несогласованность потребителей толщины стены при устаревшем/пересчитанном compatibility-key после Optimize»), а не безусловно закрывали зарепорченный визуальный симптом. Тот же нюанс стоит отразить и в финальном комментарии/закрытии #258 — новый #261 уже открыт для отдельного расследования реальной причины.
Low (снято с записью)
scripts/model-invariants.mjs,titlesвнутриreport(): ключwall_key: 'Записи толщины, которые не найдутся по ключу'теперь мёртв —wall_keyкакkindviolation больше никогда не 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.
Итог
Код и тесты для заявленного в ТЗ контракта (стабильный wallKey, строгий
same-span lookupWall, канонизация через Optimize, пересмотренная
диагностика инварианта) — корректны, полны и доказаны исполнением, включая
работающие mutation-анкеры. Единственная находка — Medium, в скоупе,
касается не кода, а точности формулировки release-артефактов, которые
безусловно заявляют устранение зарепорченного в #258 визуального симптома,
хотя собственное расследование владельца в этом же issue показывает, что
исправленный дефект не объясняет этот симптом на его реальных данных. Это
ровно случай «AC выполнены, но изменение не решает заявленный сценарий» —
жёлтый вердикт, возврат автору для правки формулировок в docs/CHANGELOG.md
и docs/CHANGELOG.ru.md (и, по желанию автора, уточнения в самом issue перед
закрытием).