Files
houseplan-card/docs/reviews/CODE-REVIEW-160-r2.md
T
claude[bot] 9cf5cdf3bd
Проверка (CI) / Классификация изменённых файлов (push) Successful in 28s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 56s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Skipped
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 56s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
docs: review document for #160
Issue: #160
User-Visible: no
2026-09-06 05:06:45 +00:00

28 KiB
Raw Blame History

CODE-REVIEW-160-r2 — Isometric Stage 3

  • Issue: https://github.com/Matysh/houseplan-card/issues/160
  • Этап: код-ревью (PROCESS.md §2.7)
  • Заход: r2 · блокирующих циклов израсходовано 1 из 4 (потрачено в r1: красный вердикт вернул задачу на правки; зелёный вердикт цикла не образует, #227)
  • Ветка: issue/160-isometric-stage3
  • SHA материала ревью: 8c12474531b7a3ed2665e00e248de24ef6748c64 (сверено git rev-parse HEAD непосредственно перед выводом, PROCESS.md §2.7)
  • Предыдущий раунд: docs/reviews/CODE-REVIEW-160-r1.md, материал SHA ee7d486924d5b1641a56f671bbf94e965fb76493 (подтверждено git merge-base --is-ancestor ee7d4869… HEAD — да, прямой предок; ребейза между раундами не было: git merge-base HEAD origin/dev = afba49a3… совпадает с базой r1)

Скоуп раунда — разбор по дельте (PROCESS.md §2.9/§2.10)

Раунд не первый, и дельта локальна: три коммита поверх материала r1, git diff ee7d4869..HEAD --stat — 3 файла продукта (src/houseplan-card.ts, src/iso-scene-render.ts, новый src/opening-symbol-placement.ts), 3 тестовых файла, docs/images/screenshots.json (только манифест), комментарий в scripts/bundle-budget.mjs, плюс пересборка бандлов и собственный документ r1. Ни ребейза на ушедший вперёд dev, ни смены контракта поведения, ни новой подсистемы — условия §2.10 для полного разбора не выполнены. Разбор ограничен находками r1 и тем, до чего дотягивается дельта (лок-бейдж проёмов, safe-point резолвер, leaf-basis, скриншоты, байты бандла); остальные 15 AC наследуются из r1 без повторной проверки (раздел ниже).

Цель раунда — закрытие H1/M1/M2/M3 из r1. Все три коммита несут Issue: #160 / User-Visible: no (сверено git show -s --format=full на каждом).

Как проверялось — гейты

Гейт Команда Результат
Typecheck npx tsc --noEmit зелёный
Unit npm test 2075 passed, 1 skipped, 0 failed (совпадает с хендоффом автора; было 2071 на r1 — +4 новых теста от фиксов M1–M3)
Build + sync npm run build && npm run bundle:sync зелёный; git status --short после пересборки чист — три копии бандла (dist, custom_components/houseplan/frontend, demo/srv/assets) байт-в-байт совпадают с закоммиченными
Bundle budget npm run bundle:budget зелёный: initial View 299495 B gzip (потолок 300000±2000, запас 1571 Б); известный долг о малом запасе (#367), не относится к #160
node scripts/check-docs.mjs обязателен диффом по src/** зелёный — «Documentation checks passed (7 files, 12 external links)». H1 закрыт, см. таблицу ниже
node scripts/no-new-any.mjs --base ee7d4869 --head HEAD новый код дельты зелёный: 52 добавленные строки в 3 файлах, новых any нет
git diff --check origin/dev...HEAD — чисто
node scripts/process-gate.mjs офлайн, диапазон origin/dev..HEAD, 7 коммитов «гейт пройден, предупреждений 0» (проверка 8 требует --issues, не запускал — не нужна ревьюеру)
node scripts/smoke-select.mjs --base ee7d4869 --head HEAD выбор смоков по дельте 6 «прямых совпадений», все — по символу cellCm (широко используемое имя, не специфично для лок-бейджа/safe-point/leaf-basis); суждение ревьюера ниже
node demo/smoke_isometric_contract.mjs суждение ревьюера: дельта правит raised/Flat lock anchor и safe-point resolver — это ровно контракт этого смока зелёный, все ассершены true
node demo/smoke_isometric_live_touch.mjs то же основание зелёный, все ассершены true
node demo/smoke_lock_invariant.mjs смежная поверхность: правка трогает код, вычисляющий позицию lock-бейджа (SCOPE.md lock invariant) зелёный
node demo/smoke_lock_action.mjs то же основание зелёный
Мутационная проверка «тест умеет падать» (M1/M2/M3, лично) см. раздел ниже 2 из 3 — красный получен ровно на новом тесте; 1 из 3 — красный получен на соседнем непеределанном тесте, не на новом (см. Находку L1)

Закрытие раунда r1

Находка r1 Чем закрыта Где это видно
H1 — check-docs красный, скриншоты не пересняты Каноническая Linux-съёмка через job Docs screenshots на коммите bd60edd2 (fix-коммит), приёмка npm run docs:accept -- --reviewed, новый sourceFingerprint закоммичен docs/images/screenshots.json (24 строки, только хеши отпечатка) в коммите 8c124745; независимо проверено мной по логу workflow-прогона (см. ниже) и локальным запуском node scripts/check-docs.mjs → «passed»
M1 — два независимых источника формулы floor anchor лок-бейджа проёма Формула вынесена в openingLockFloorPlacement() (src/opening-symbol-placement.ts:44-59); houseplan-card.ts:13086-13093 и iso-scene-render.ts:856-874 вызывают её напрямую вместо дублирования математики; добавлен parity-тест src/opening-symbol-placement.ts (новый экспорт), git diff на обоих сайтах вызова, test/opening-symbol-placement.test.mjs:33-52 — лично мутировал знак flipV-ветки, новый тест покраснел («not ok 2»)
M2 — реальный алгоритм isoRoomSafePoint (grid-search) не покрыт тестом Добавлены прямые вызовы isoRoomSafePoint(room) без предзаданного safePoint для донат-комнаты и вогнутой комнаты, плюс вырожденный кейс test/iso-overlays.test.mjs:96-133 — прогнано лично, все проходят; но мутационная проверка показала ограничение доказательства для hole-пути, см. Находку L1
M3 — leafBasis flipH не тестируется независимо от flipV Заменённый тест строит normal/flipH-only/flipV-only/both и сравнивает точные hinge/closedVector/quarterVector test/iso-openings.test.mjs:209-235 — лично мутировал источники sx/sy (поменял местами flipH↔flipV), новый тест покраснел («not ok 10»)

Все три Medium и единственный High из r1 закрыты предъявленной строкой кода или теста, а не заявлением автора; каждое закрытие перепроверено мной лично (включая намеренную порчу защиты там, где это дёшево — юниты).

Мутационная проверка (лично, «тест умеет падать»)

Выполнено через git worktree-независимую правку рабочего дерева (не Edit-инструментом — редактирование продуктового кода мне не разрешено как постоянное действие; правки делались временно, прогонялись и полностью откатывались побайтово, git status --short после отката пуст, см. лог команд ниже):

  1. M1 (opening-symbol-placement.ts): flipV ? -1 : 1 → flipV ? 1 : -1. test/opening-symbol-placement.test.mjs → not ok 2 - opening lock floor placement owns both the anchor and its host side. Красный подтверждён.
  2. M3 (iso-openings.ts::leafBasis): поменял местами источники sx(было flipH)↔sy(было flipV, кроме gate). test/iso-openings.test.mjs → not ok 10 - flipH and flipV independently mirror their exact structural axes. Красный подтверждён.
  3. M2 (iso-overlays.ts::pointStrictlyInRoom): убрал цикл проверки дыр (for (const hole of room.holes...)). Результат неожиданный — новый тест test/iso-overlays.test.mjs:96 («safe-point search… stays… inside… holes») остался зелёным; красным стал другой, не тронутый этой задачей тест — not ok 3 - strict room ownership excludes holes, shared boundaries and outside points (уже существовал в r1, вызывает resolveIsoOverlayOwner напрямую с точкой в дыре). См. Находку L1 — новый M2-тест не независим от собственно проверяемого свойства.

Все три файла возвращены оригинальным содержимым сразу после соответствующего прогона; после восстановления перепрогнаны все три тестовых файла целым набором — 27/27 зелёных, git status --short пуст.

Находки

L1 — новый M2-тест не независимо доказывает «вне любой дыры» (Low, запись без блокировки)

test/iso-overlays.test.mjs:96-114 вычисляет first = isoRoomSafePoint(room), затем проверяет принадлежность через resolveIsoOverlayOwner({floorAnchor: first, rooms:[room]})?.id === room.id. Но resolveIsoOverlayOwner определяет принадлежность вызовом того же pointStrictlyInRoom (iso-overlays.ts:231,233), который isoRoomSafePoint использует внутри своего consider() (iso-overlays.ts:202) для отбора кандидатов. Если у pointStrictlyInRoom сломать именно проверку дыр (мутация выше), обе функции ошибаются согласованно друг с другом: isoRoomSafePoint вернёт точку внутри дыры, а resolveIsoOverlayOwner её не отклонит — тест не заметит.

Заявленный в r1 риск («если алгоритм вернёт точку в дыре, ни один тест этого не заметит») сейчас закрыт не тем тестом, который претендует на это в своём названии, а старым, непеределанным test 3 того же файла — оно продолжает проверять pointStrictlyInRoom через явный floorAnchor:[50,50], независимо от isoRoomSafePoint. Защита существует и я лично подтвердил, что она срабатывает (мутация ловится набором тестов файла в целом), но она не там, где её называет новый тест, и последующий рефакторинг, который переименует или уберёт «чужой» test 3, тихо унесёт с собой и защиту M2, а новый тест продолжит быть зелёным.

Не поднимаю до Medium: реальной прорехи в текущем дереве нет (мутация ловится), primary-риск r1 («алгоритм вообще не выполняется в тестах») закрыт по существу — до этой правки ни один тест не проходил через сам candidate/grid-search, теперь проходит. Это узкое дефектное свойство конкретно нового теста (assertion пишет о доказательстве, которого сам не даёт), тот же класс, что и Low-находки r1 (например «shadows не защищено собственным тестом… будущий рефакторинг тихо сломает»).

Рекомендация (не блокирует): заменить/дополнить проверку в test/iso-overlays.test.mjs:110-113 прямым геометрическим утверждением, не зависящим от pointStrictlyInRoom — например явно проверить, что first лежит вне bbox дыры [35,65]×[35,65] для donut, отдельной инлайн-проверкой.

L2 — приёмка скриншотов сменила заявленную платформу на win32 без видимого основания в хендоффе (Low, запись без блокировки)

docs/images/screenshots.json:121 — "acceptedOn" сменился с "linux" (значение до правки r2) на "win32". Код scripts/docs-accept.mjs вызывает assertCaptureEnvironment({kind:'docs', stage:'accept'}) первым действием (scripts/capture-environment.mjs:110-119), которое бросает исключение и останавливает приёмку на платформе, отличной от linux, если не задан HP_ALLOW_FOREIGN_CAPTURE=<причина> (пустая причина не считается). То есть запись acceptedOn: "win32" в закоммиченном файле механически доказывает, что приёмка либо шла через явный, осознанный обход этой переменной (легитимный путь — ровно то, для чего он существует: артефакт снят на Linux CI, а сама команда приёмки выполнена локально на Windows, дневном окружении автора по AGENTS.md), либо гейт был обойдён иначе. Скрипт не сохраняет саму причину в JSON (только platform), поэтому из закоммиченного дерева её не увидеть.

Хендофф-комментарий автора («manifest закоммичен в 8c124745») не называет ни команду приёмки, ни обход, ни его причину — только результат. Я не нахожу признаков нелегитимности: воспроизведение самого содержательного свойства — байт-в-байт совпадение всех 10 PNG — я проверил независимо по логу CI-прогона (git diff --check origin/dev PNG: 0 изменившихся, тот же Chromium 151.0.7922.34, тот же oxipng 10.2.0 — см. ниже), так что H1 закрыт по существу вне зависимости от площадки приёмки. Но провенанс самой команды приёмки (в духе дисциплины «verified без названной команды не доказательство») неполон — это узко процессная, а не продуктовая находка.

Не поднимаю до Medium: PROCESS.md/AGENTS.md явно требуют трейлер Baseline-Reviewed только для правок demo/golden/baselines/**; для docs/images/** эквивалентного машинного требования нет, и содержательное свойство (неизменность байтов) я проверил независимо. Рекомендация (не блокирует): в будущих хендоффах называть точную команду приёмки, включая HP_ALLOW_FOREIGN_CAPTURE, если он был нужен.

Независимая проверка H1 по логу CI (не на слово)

gh api repos/Matysh/houseplan-card/actions/runs/34012427938 --jq '.event,.status,.conclusion'
→ workflow_dispatch, completed, success

# фактически выбранный actions/checkout ref и итоговый коммит:
ref: issue/160-isometric-stage3
git log -1 --format=%H → bd60edd23818eac617857de27260dc604e430bfc   # ровно fix-коммит r2

# шаг «Вердикт» джобы:
 M docs/images/screenshots.json
--- изменившихся PNG: 0
--- Chromium: было «151.0.7922.34», стало «151.0.7922.34»
--- oxipng: было «oxipng 10.2.0», стало «oxipng 10.2.0»
ВЕРДИКТ: ничего не изменилось, принимать нечего.

Это подтверждает независимо от заявления автора: приёмка не «пробила» новую съёмку под видом старой — PNG не менялись ни одним байтом, менялся только метаданный отпечаток исходников (соответствует тому, что диапазон меняет src/**, а не визуальный вывод).

Что проверено и корректно (дельта r2)

  • Формула lock anchor (M1): идентична побайтово прежним двум копиям — сверил построчно; вызовы в houseplan-card.ts и iso-scene-render.ts передают одинаковые поля (x/y/angle/flipV/gateFace), gateFace вычисляется так же, как раньше (_openingFace/partitionOpeningFace), просто на вызывающей стороне вместо внутри общей функции — не архитектурный сдвиг.
  • Живой DOM подтверждает отсутствие регрессии: smoke_isometric_contract и smoke_isometric_live_touch (комплексные проверки raised/Flat геометрии, включая lock-бейдж и safe-point/nudge путь) зелёные на пересобранном бандле этой дельты.
  • Смежная безопасность (SCOPE.md lock invariant) не задета: код actuation-пути (resolveToggleIntent, isControllable, _cardToggle) в дельте не тронут; smoke_lock_invariant/smoke_lock_action зелёные.
  • Бюджет бандла: сдвиг с 299464 → 299495 Б gzip (+31 Б, ожидаемо — добавленный import { gridVisualUnits } в opening-symbol-placement.ts и новый экспорт) не поднимает потолок и не приближается к аварийному порогу сверх уже известного долга #367; комментарий в bundle-budget.mjs обновлён соответствующими числами (не код, чисто документирующий комментарий).
  • Одно число — один источник: M1 устраняет ровно риск, который правило называет — два места считали одну видимую величину (позицию лок-бейджа) независимо; теперь она в одном месте. Проверил, что test/single-source-numbers.test.mjs не тронут (ожидаемо — задача не меняет формат отображаемого числа).
  • Трейлеры: все три коммита дельты — Issue: #160 / User-Visible: no, соответствует «Stage 3 остаётся скрытой» (публичные changelog не менялись).

Унаследовано из r1 (без повторной проверки)

Документ: docs/reviews/CODE-REVIEW-160-r1.md, материал SHA ee7d486924d5b1641a56f671bbf94e965fb76493 (подтверждён как прямой предок текущего HEAD). Дельта не касается ни одной из перечисленных ниже поверхностей (проверено по git diff ee7d4869..HEAD --stat — только src/houseplan-card.ts, src/iso-scene-render.ts, src/opening-symbol-placement.ts, 3 тестовых файла, манифест скриншотов, комментарий bundle-budget.mjs):

  • AC2/D1 камера (rotDeg=4, tiltDeg=20, единая матрица isoPlaneMatrix/applyIsoMatrix, src/iso-projection.ts) — не тронуто.
  • AC3/D2 raised/floor split, включая vacuum floor-bound — не тронуто (кроме самого anchor lock-бейджа, который переразобран заново выше).
  • AC4 плейт/44×44 hit target — не тронуто.
  • AC6/D5 проёмы (jamb/reveal/leaf/frame/sill, hinge/face turn direction, iso-scene-render.ts W9-путь) — не тронуто (leaf-basis M3 переразобран заново выше, это смежная, но отдельная от W9 функция).
  • AC7 show_borders:false — не тронуто.
  • AC8/AC12 structural fingerprint, LRU cap 8, material defs O(1) — не тронуто.
  • AC9 degradation (forced-colors/no-filter, fail-closed Flat fallback) — не тронуто.
  • AC10 Zigbee-топология на поднятых DOM-центрах — не тронуто.
  • AC11 lifecycle/touch/kiosk — не тронуто (смежная безопасность перепроверена смоками выше, но не как часть этого пункта).
  • AC13 негативный контракт (нет новых зависимостей/i18n/storage/config/ network путей, legacy hp-labs/expiry не возвращены) — не тронуто.
  • AC14/AC15 гейты, golden-диагностика, документация подсистемы (кроме самого отпечатка скриншотов, переразобранного как H1 выше) — не тронуто; npm run golden:verify в этом раунде не перезапускал: делта не меняет визуальную геометрию/рендер (чистый рефакторинг формулы + тесты), а живой DOM-контракт (смоки выше) и byte-identical PNG (независимая проверка по логу CI) уже показывают отсутствие визуального сдвига; полный golden — не гейт ревью, а предрелизный (PROCESS.md §8).
  • Производительность (§10, sanity-прогон isometric-stage3-dense-v1) — не тронуто; делта не меняет структуру рендера, только формулу позиционирования и покрытие тестами.

Чего не проверял и почему

  • Полный npm run golden:verify — не перезапускал в этом раунде (см. «Унаследовано» выше); было исчерпывающе проверено в r1 на материале, который дельта не меняет визуально.
  • Полный набор demo/smoke_*.mjs (226 файлов) — не прогонял; выборка по дельте (smoke-select.mjs) не дала специфичных совпадений (только общий символ cellCm), прогнал по собственному суждению два целевых контрактных смока плюс два смежных lock-смока — все зелёные. Полный прогон — предрелизный объём (PROCESS.md §8), не гейт ревью для этой по размеру небольшой дельты.
  • npm run invariants / model-invariants.mjs — дельта не меняет геометрическую модель (комнаты/стены/layout/marker.space/open_spans); подтверждено diff-статом, ни одного изменения в custom_components/** или схеме конфига.
  • python -m pytest tests_backend — ноль изменений в custom_components/houseplan/**/*.py в этом диапазоне.
  • 7-sample exact-SHA performance профили — канонический гейт остаётся pre-beta (§8/§10); дельта не меняет структуру рендера/кэша, только формулу позиционирования и тестовое покрытие, повторный sanity-прогон не требовался суждением ревьюера.
  • Точная причина HP_ALLOW_FOREIGN_CAPTURE для приёмки скриншотов на win32 — не могу восстановить из закоммитированного дерева (скрипт не пишет причину в манифест, только платформу); зафиксировано как L2.

Итог

High: 0. Medium: 0. Все четыре находки r1 (1 High + 3 Medium) закрыты предъявленной строкой кода/теста и лично перепроверены — три из них через намеренную порчу защиты с последующим красным прогоном и откатом, одна (H1) через независимую сверку лога CI-прогона по точному коммиту. Дельта не вносит новых High/Medium; две узкие Low-находки (L1 — новый M2-тест доказывает hole-safety не независимо от проверяемой функции; L2 — провенанс площадки приёмки скриншотов не назван в хендоффе) записаны без блокировки — реальная защита в обоих случаях сейчас присутствует, риск умозрительный/процессный, не продуктовый.

Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0


Материал раунда

  • Ветка: issue/160-isometric-stage3, коммит 8c12474531b7 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: b09988aa9a28648b66b731e535ddb243372428c3
    git log --all --format='%H %T' | grep b09988aa9a28
    
  • ТЗ docs/specs/160-isometric-stage3.md, блоб 20ed447b8f58bdfe9e48695584ff616c36b79b4d
    git log --all --find-object=20ed447b8f58bdfe9e48695584ff616c36b79b4d -- docs/specs/160-isometric-stage3.md