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

23 KiB
Raw Blame History

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). Реализация всё равно доведена по первоначальному контракту ТЗ — это осмысленное решение (правит реальный, отдельно доказанный дефект класса «одно ребро — разные ответы у потребителей»), но релиз-артефакты этой задачи заявляют, что именно зарепорченный симптом устранён. Разбор ниже отдельно оценивает код (полностью корректен по контракту ТЗ) и это продуктовое несоответствие.

Как проверялось

  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 симптом устранён.
  • Сценарий, в котором это ломается: владелец обновляется на бету с этим коммитом, открывает свой реальный план (тот самый, со скриншотов), нажимает «Оптимизировать» — и, по его же собственному измерению (комментарий), 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 как 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.

Итог

Код и тесты для заявленного в ТЗ контракта (стабильный wallKey, строгий same-span lookupWall, канонизация через Optimize, пересмотренная диагностика инварианта) — корректны, полны и доказаны исполнением, включая работающие mutation-анкеры. Единственная находка — Medium, в скоупе, касается не кода, а точности формулировки release-артефактов, которые безусловно заявляют устранение зарепорченного в #258 визуального симптома, хотя собственное расследование владельца в этом же issue показывает, что исправленный дефект не объясняет этот симптом на его реальных данных. Это ровно случай «AC выполнены, но изменение не решает заявленный сценарий» — жёлтый вердикт, возврат автору для правки формулировок в docs/CHANGELOG.md и docs/CHANGELOG.ru.md (и, по желанию автора, уточнения в самом issue перед закрытием).