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

301 lines
28 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-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**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `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
```