Issue: #160 User-Visible: no
28 KiB
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, материал SHAee7d486924d5b1641a56f671bbf94e965fb76493(подтверждено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 после отката пуст, см. лог
команд ниже):
- 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. Красный подтверждён. - 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. Красный подтверждён. - 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.tsW9-путь) — не тронуто (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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
b09988aa9a28648b66b731e535ddb243372428c3git log --all --format='%H %T' | grep b09988aa9a28 - ТЗ
docs/specs/160-isometric-stage3.md, блоб20ed447b8f58bdfe9e48695584ff616c36b79b4dgit log --all --find-object=20ed447b8f58bdfe9e48695584ff616c36b79b4d -- docs/specs/160-isometric-stage3.md