mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
383 lines
32 KiB
Markdown
383 lines
32 KiB
Markdown
# CODE-REVIEW-132-r1
|
||
|
||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/132
|
||
- **Связанный bug в том же scope:** #185 (закрывается тем же кодом, по решению владельца)
|
||
- **Диапазон:** `origin/dev...HEAD` на ветке `issue/132-partition-openings-v2`,
|
||
коммиты `b9bf210..9f77e3e` (5 коммитов: 4 документационных + 1 продуктовый —
|
||
`9f77e3e feat: support openings in independent walls`)
|
||
- **ТЗ:** `docs/specs/132-partition-openings.md`, зелёное ревью
|
||
`docs/reviews/SPEC-REVIEW-132-r1.md`
|
||
- **Роль:** ревьюер кода (не автор), этап `S7-code-review`
|
||
- **Цикл:** r1/4
|
||
|
||
## Скоуп ревью
|
||
|
||
Диапазон правит 43 файла (2961 / 603): новый резолвер хоста
|
||
(`src/partition-openings.ts`), геометрию cut (`src/physical-geometry.ts`,
|
||
`src/wall-thickness.ts` косвенно), размещение (`src/opening-placement.ts`,
|
||
`src/align-grid.ts`), структурную топологию комнат/#185
|
||
(`src/plan-snap-overlay.ts`, `src/houseplan-card.ts`), свет/Glow/sun
|
||
(`src/houseplan-card.ts`, `src/styles.ts`), HA-состояние и команды
|
||
move/delete/undo (`src/houseplan-card.ts`), backend-валидацию и
|
||
import/export/websocket (`custom_components/houseplan/*.py`), i18n
|
||
(`src/i18n/en.json`, `ru.json`), 9 канонических документов + оба changelog,
|
||
и полный слой автотестов (unit/backend/smoke).
|
||
|
||
Проверялось соответствие 12 AC из ТЗ (§20), контракту `docs/SCOPE.md`
|
||
(lock-инвариант, J1–J4), каноническим документам подсистем и трейлерам
|
||
коммитов.
|
||
|
||
## Как проверялось
|
||
|
||
Дешёвые гейты (прогнаны лично, точные команды и результат):
|
||
|
||
| Гейт | Команда | Результат |
|
||
|---|---|---|
|
||
| Typecheck | `npx tsc --noEmit` | зелёный, без вывода |
|
||
| Unit | `npm test` | `870 passed / 0 failed` |
|
||
| Build + сверка бандлов | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | сборка ок, обе копии побайтово совпадают |
|
||
|
||
Смоки, прогнанные локально (по diff и AC — не весь набор из 141):
|
||
|
||
| Смок | Результат | Почему выбран |
|
||
|---|---|---|
|
||
| `node demo/smoke_partition_openings.mjs` | OK, все 12 полей `true` | новый, основное доказательство AC1–AC3, частично AC5 |
|
||
| `node demo/smoke_room_autoclose.mjs` | OK, все 9 полей `true` | изменён, доказательство #185/AC5 |
|
||
| `node demo/smoke_opening_preview.mjs` | OK, все 39 полей `true` | изменён, placement/AC1 |
|
||
| `node demo/smoke_glow.mjs` | OK | AC4, свет |
|
||
| `node demo/smoke_inert_openings.mjs` | OK | AC8, passage inert |
|
||
| `node demo/smoke_opening_binding.mjs` | OK | AC8, HA/security |
|
||
| `node demo/smoke_opening_tunnel_fill.mjs` | OK | AC2, tunnel/cut |
|
||
| `node demo/smoke_wall_junctions.mjs` | OK | AC2, junction patches |
|
||
| `node demo/smoke_unified_wall_tool.mjs` | OK | AC12, регресс #173 |
|
||
| `node demo/smoke_isometric_contract.mjs` | OK | AC7, Iso renderer |
|
||
| `node demo/smoke_openwall.mjs` | OK | AC12, регресс |
|
||
| `node demo/smoke_sun.mjs` | OK | AC14/sun regression |
|
||
|
||
Остальные 129 смоков не запускались — задача не задевает все поверхности
|
||
разом (см. таблицу выше — выбор покрывает placement/geometry/topology/light/
|
||
HA/regression, по одному представителю на AC-кластер).
|
||
|
||
`npm run golden:verify` (после свежей сборки и копирования бандла) —
|
||
запущен, поскольку diff меняет геометрию/свет/рендер. Результат: 60/62
|
||
сцен `passed`, одна `different` (`plan-snap-line-gaps-dark`), один
|
||
транзиентный `error` при первом прогоне (`openings-filled-tunnel-dark`),
|
||
исчезнувший при повторном прогоне на том же дереве (похоже на конкуренцию
|
||
ресурсов при параллельных фоновых процессах ревью, не на дефект — см.
|
||
«Чего не проверял»). Эталоны `demo/golden/baselines/**` в этом диапазоне
|
||
**не менялись** (подтверждено `git diff --stat`), что соответствует
|
||
процессу: обновление golden — предрелизный шаг через
|
||
`golden:accept --reviewed`, не часть этого код-ревью.
|
||
|
||
`python -m pytest tests_backend -q` — прогнан после ручной установки
|
||
отсутствовавших в этом окружении `pytest`/`voluptuous` (`homeassistant` не
|
||
установлен). Результат: **139 passed**, но это только «чистое» подмножество
|
||
— `conftest.py` молча игнорирует `test_ha_*.py`, когда `homeassistant`
|
||
недоступен (задокументированное в `AGENTS.md` поведение). Значит,
|
||
`tests_backend/test_ha_import_export.py` (изменённый в этом диапазоне файл,
|
||
+4/−…) **не выполнялся** в этом прогоне. Учитывая, что именно в
|
||
`import_export.py` найден блокирующий High (см. ниже), это существенное
|
||
ограничение — см. «Чего не проверял».
|
||
|
||
Дополнительно организовано 7 независимых агентских разборов (каждый —
|
||
отдельная поверхность: геометрия/cut, топология комнат #185, свет/Glow,
|
||
HA-state/move-delete-undo, backend-валидация, качество тестов/смоков,
|
||
документация/i18n), каждый со своими file:line-цитатами и с явным заданием
|
||
мысленно отменить конкретную строку продукта и проверить, падает ли
|
||
привязанный тест. Их находки перепроверены мной лично чтением исходников
|
||
(конкретные цитаты — в разделе «Находки»); один инцидент — агент по ошибке
|
||
выполнил `git checkout -- src/physical-geometry.ts` в общем чекауте поверх
|
||
активной сборки и вручную восстановил строку — проверен: `git status
|
||
--short` и `git diff HEAD` после инцидента пусты, рабочее дерево совпадает с
|
||
HEAD, порчи нет.
|
||
|
||
## Находки
|
||
|
||
### High-1 — импорт/дублирование space с partition-hosted проёмом всегда отклоняется
|
||
|
||
**Файл:** `custom_components/houseplan/import_export.py:830–845` (ремап id),
|
||
согласовано с `custom_components/houseplan/validation.py:836–839`
|
||
(референциальная проверка `host.id`).
|
||
|
||
`build_space_merge()` — код пути «Backup → импорт одного space» (вызывается
|
||
из `create_preview()` для `document["kind"] == "space"`, `import_export.py:
|
||
1153` — это реальная пользовательская фича экспорта/импорта одной комнаты/
|
||
этажа, представленная в golden-сценах `backup-*`). Цикл ремапа id
|
||
(`:830–838`) проходит по коллекциям `rooms, room_drafts, partitions,
|
||
wall_columns, openings, decor` и переписывает **собственный** `item["id"]`
|
||
каждой записи на свежий id, но нигде не переписывает вложенную ссылку
|
||
`opening["host"]["id"]`, которая указывает на **старый** id partition.
|
||
|
||
Далее `merged_config = CONFIG_SCHEMA(merged_config)` (`:977`) прогоняет
|
||
`_space_geometry_invariants` (`validation.py:813–853`), который для каждого
|
||
`opening.host.id` требует существующий partition в том же space
|
||
(`validation.py:836–839: raise vol.Invalid(...)`). После ремапа
|
||
`opening.host.id` физически не может совпасть ни с одним новым id partition
|
||
— значит **любой** импорт/дублирование space, содержащего хотя бы один
|
||
partition-hosted проём, безусловно завершается `ImportFailure("invalid_config",
|
||
...)`.
|
||
|
||
Проверено лично чтением: `grep -n "host" custom_components/houseplan/
|
||
import_export.py` даёт ровно 2 упоминания вне ремап-цикла — `:269` (простое
|
||
сохранение поля при экспорте, ремапа id не требует) и `:985` (вызов
|
||
`validate_partition_opening_hosts`, который проверяет только «host не исчез
|
||
у уже существовавшего проёма», а не референциальную целостность после
|
||
ремапа — эту проверку делает уже упомянутый `_space_geometry_invariants`
|
||
через `CONFIG_SCHEMA`). Ни один call site не трогает вложенный `host.id`.
|
||
|
||
**Тест:** `tests_backend/test_ha_import_export.py:876–909`
|
||
(`test_space_merge_remaps_every_space_owned_id_and_room_link`) — единственный
|
||
тест, гоняющий ремап id через `build_space_merge`, и его фикстура-проём
|
||
(`op1`, строка ~885) намеренно не имеет `host`. Регрессия непокрыта. Этот же
|
||
тестовый файл в принципе не выполнялся в доступном окружении ревью (нет
|
||
`homeassistant`) — см. «Чего не проверял»; сама находка получена чтением
|
||
кода и подтверждена независимым агентским разбором, не прогоном теста.
|
||
|
||
**Почему High:** это прямое нарушение AC9 («host fields survive save/export/
|
||
import/optimize») и §18 ТЗ («Export/import/backup сохраняют host object и
|
||
referential order»); ломается не крайний случай, а любое использование
|
||
только что реализованной фичи через уже существующий, регулярно
|
||
используемый путь backup/duplicate. Блокирует.
|
||
|
||
**Как чинить (для автора, не мой домен):** при ремапе `openings` в этом же
|
||
цикле переписывать `item.host.id = id_map[item.host.id]`, когда `host.kind
|
||
=== 'partition'`, аналогично тому, как уже переписывается `room.open_to`
|
||
через `old_room_ids` (`:839–844`) и `marker.room_id` (`:875–876`).
|
||
|
||
## Medium-находки (заведены отдельными issue)
|
||
|
||
### Medium-1 → #186 — нет jamb safety margin
|
||
|
||
`src/partition-openings.ts:41` (`jambMargin = 0`, не переопределяется ни на
|
||
одном из ~9 call sites: `src/houseplan-card.ts:7290, 7751, 7783, 11094,
|
||
11393, 11402, 17301`, `src/space-render.ts:214`) и
|
||
`custom_components/houseplan/validation.py:840–846` (только `1e-9`
|
||
float-допуск). ТЗ §7/§16/§23 явно называет jamb safety margin отдельно от
|
||
допуска на погрешность. Проём можно поставить впритык к концу перегородки
|
||
без зазора на откос. Функционально не ломает (fail-dark по-прежнему
|
||
работает), но расходится с принятым ТЗ. Заведено: #186.
|
||
|
||
### Medium-2 → #187 — fallback-гвард источника света не fail-dark «по построению»
|
||
|
||
`src/houseplan-card.ts:14585` (`pointInOpaquePlanBody(sourcePoint,
|
||
masonryGeometry, physical)`) использует в качестве fallback
|
||
`physical = this._physicalBodiesR(space)` (`:14527`) — тело, вырезанное по
|
||
**всем** типам hosted-проёмов без различия света, а не
|
||
light-policy-фильтрованный `lightPhysical` (`:14442–14451`), который
|
||
`_lightBarriers` использует для основной геометрии. Обнаружено, что
|
||
параметр `physical` внутри функции больше нигде не используется (мёртвый
|
||
после рефакторинга, кроме этого места). Если `wallBodiesGeometry()`
|
||
(`src/wall-thickness.ts:1790–1792`) выбросит исключение, `masonryGeometry`
|
||
станет `[]`, и guard будет опираться на дырявое (в т.ч. по окну) тело —
|
||
контракт «boolean failure fail-dark» (ТЗ §13/§17) в этой ветке держится на
|
||
случайной само-ограниченности развёртки, а не на архитектуре. Практическое
|
||
проявление маловероятно (требует throw в объединении), поэтому Medium, не
|
||
High. Заведено: #187.
|
||
|
||
### Medium-3 → #188 — тест junction-patch не умеет падать
|
||
|
||
`test/partition-openings.test.mjs:64–82` («computed junction patches cannot
|
||
bridge a hosted slot») не отражает регрессию в
|
||
`src/physical-geometry.ts:206–208` (`.flatMap((body) =>
|
||
cutPartitionBody(body, partitionCuts, epsilon))` на join-patches): проверено
|
||
мысленным (и одним агентом — фактическим) удалением этой строки — все 8
|
||
подтестов остаются зелёными, так как bbox патча в T-образной фикстуре не
|
||
достигает проверочной точки `[92,0]` независимо от вырезания. Сам
|
||
production-код при этом корректен — я перечитал `physical-geometry.ts:186–
|
||
211` лично и подтверждаю: патчи действительно прогоняются через
|
||
`cutPartitionBody` со всеми `partitionCuts`, до объединения в `all`
|
||
(`:209`), т.е. заявленное в §10 ТЗ поведение реализовано верно, только не
|
||
доказано этим конкретным тестом. Заведено: #188.
|
||
|
||
## AC1–AC12 — разбор по каждому критерию
|
||
|
||
1. **AC1 (Walls workflow/placement).** Доказано: unit
|
||
(`test/opening-placement.test.mjs` — партиционный candidate побеждает
|
||
room-wall при точном совпадении/collinear, отклоняется при
|
||
пересечении/неоднозначности; `test/partition-openings.test.mjs` — резолвер
|
||
центра/угла/длины/depth) + smoke (`smoke_partition_openings.mjs`,
|
||
`smoke_opening_preview.mjs`, оба зелёные). Active draft/column/virtual span
|
||
как host отклоняются — подтверждено чтением `resolvePartitionOpening`
|
||
(принимает только `PartitionCfg`) и юнитами placement. **Пройдено.**
|
||
2. **AC2 (geometry/composite overlap).** Full-depth cut для 1/15/100 см и
|
||
diagonal — доказано unit (`test/partition-openings.test.mjs:52–73`) и
|
||
смоком `smoke_opening_tunnel_fill.mjs`. Composite double-cut (partition +
|
||
coincident room-wall режутся оба) — проверено **чтением, не
|
||
исполнением**: `_roomWallOpeningInputs` (`houseplan-card.ts:7795–7816`)
|
||
эмитит room-wall cut только при точном collinear-покрытии
|
||
(`partitionOpeningHasCompositeRoomWall`), а независимое тело partition
|
||
режется параллельно через `_partitionOpeningCuts`; оба пути сходятся в
|
||
`wallBodiesGeometry`/`_wallUnionGeometry`. Отдельного unit/smoke именно на
|
||
coincident-случай нет (см. «Чего не проверял»), но код-путь однозначен и
|
||
golden-сцены `openings-thick-wall-dark`, `wall-junctions-*-dark`,
|
||
`geometry-diagonal-45-opening-dark` прошли без изменений — косвенно
|
||
подтверждает. Junction patches не закрывают cut заново — код корректен
|
||
(см. Medium-3 про сам тест). **Пройдено**, с оговоркой Medium-3.
|
||
3. **AC3 (host lifecycle).** Rigid move — чисто трансляция, `t`/length
|
||
инвариантны по построению (`_physicalUp`, `houseplan-card.ts:7266–7317`).
|
||
Delete-с-confirmation: единственный путь удаления partition с hosted
|
||
openings — `_deletePhysicalSelection` → `_partitionDeleteDialog` →
|
||
`_confirmPartitionDelete`, список отсортирован по `t`
|
||
(`:7104`), Cancel — чистый no-op, Confirm атомарен, один `_recordGeometry`
|
||
вызов на всю операцию. Других путей удаления, обходящих confirmation, не
|
||
найдено — `_dropLegacySegments()` чистит только структурно некорректные
|
||
partitions и не трогает `openings` (в этом случае backend
|
||
`_space_geometry_invariants` отклонит сохранение, а не тихо потеряет
|
||
данные). Undo/Redo — единый `CommandStack` снапшот всего пространства.
|
||
Доказано unit (delete/undo команды) + smoke
|
||
(`smoke_partition_openings.mjs`: `deleteRequiresAccessibleListDialog`,
|
||
`deleteCancelIsMutationFree`, `deleteConfirmCascadesHostAndOpening`,
|
||
`deleteUndoRestoresHostAndOpening`, `deleteRedoCascadesAgain` — все
|
||
`true`, и мутационно подтверждено: удаление cascade-фильтра в
|
||
`_confirmPartitionDelete` заваливает именно эти два поля). **Пройдено.**
|
||
4. **AC4 (light).** Interior door/gate/passage пропускают Glow через полный
|
||
composite cut, exterior/window остаются opaque, invalid host fail-dark —
|
||
подтверждено чтением (`_lightBarriers`, `_roomWallOpeningInputs`,
|
||
`_partitionOpeningCuts`) и смоком `smoke_glow.mjs`. **Пройдено**, с
|
||
оговоркой Medium-2 (fallback-ветка) и отсутствием прямого unit-теста на
|
||
cache fingerprint/composite-Glow сценарий (см. «Чего не проверял»).
|
||
5. **AC5 (#185 room closure).** Реальный фикс — не в
|
||
`plan-snap-overlay.ts` (не изменился по логике), а в удалении
|
||
type-agnostic `_planSnapOpeningCuts()` из вызова
|
||
`buildPlanSnapGeometry()` в `houseplan-card.ts`: раньше каждый opening
|
||
резал структурную ось, теперь `roomCuts` строится только из реальных
|
||
`open_spans`. Подтверждено эмпирически (один из агентов подменил бандл на
|
||
собранный из `origin/dev` и прогнал обновлённый
|
||
`smoke_room_autoclose.mjs` — assertion `openingKeepsStructuralAutoClose`
|
||
стала `false` на старом бандле и `true` на текущем: воспроизводимо
|
||
красный → зелёный переход именно от этого коммита, как и требует
|
||
доказательство AC5 в ТЗ). Для partition-хостов структурная непрерывность
|
||
тривиальна: `buildPlanSnapGeometry` никогда не резал partition-оси
|
||
opening-катами (`cuts: []` для partition-источников что до, что после).
|
||
**Пройдено**, но матрица неполна: `smoke_room_autoclose.mjs` и
|
||
`smoke_partition_openings.mjs` гоняют только `type: 'door'`; window/gate/
|
||
passage и полный `_wallFaceBatch`/`_roomDialog` UI-путь для
|
||
partition-хоста явно не прогнаны (для partition проверена только
|
||
структурная снапшот-функция, не UI-диалог). Логика type-agnostic, дефекта
|
||
не вижу при чтении, но заявленная в §21 ТЗ «матрица по всем четырём
|
||
типам» не покрыта тестами буквально — фиксирую как незакрытый пробел
|
||
evidence, не как блокирующую находку (код прочитан и корректен).
|
||
6. **AC6 (no passive topology mutation).** `_offerWallFaces()` вызывается
|
||
только из двух click-обработчиков внутри Walls-инструмента
|
||
(`houseplan-card.ts:6719, 6827`), ни разу — из lifecycle/hass-сеттеров/
|
||
`_editOpening`/`_saveOpening`. Ни один из этих call sites не менялся в
|
||
этом diff. **Пройдено, проверено чтением.**
|
||
7. **AC7 (render parity).** Golden прошёл на всех Plan/Iso/View-сценах, кроме
|
||
одной «different» (см. ниже). `smoke_isometric_contract.mjs` зелёный.
|
||
Единый resolver (`resolvePartitionOpening`) используется и в
|
||
`space-render.ts`, и в `houseplan-card.ts` рендер-путях — подтверждено
|
||
чтением обоих файлов. **Пройдено.**
|
||
8. **AC8 (HA/security parity).** `resolveHaBindingStatus`, `_lockAction`,
|
||
`_renderOpeningInfoCard` не ветвятся по `host.kind`, только по `o.type`;
|
||
`_openingsR` резолвит partition-hosted openings в тот же `RenderOpening`
|
||
через spread, не параллельной реализацией. Перемещение host не трогает
|
||
`entity`/`flip_*`/id (`materializePartitionOpening` переписывает только
|
||
`x/y/angle`). Доказано чтением + smoke (`smoke_inert_openings.mjs`,
|
||
`smoke_opening_binding.mjs`). **Пройдено.**
|
||
9. **AC9 (compatibility).** Backend-схема реально проверяет существование
|
||
`host.id` среди partitions пространства, границы `t`, вписывание длины,
|
||
пересечения между hosted-проёмами одного host — не поверхностно (
|
||
`validation.py:813–853`), тест на отклонение реален (падает при удалении
|
||
`raise`, `tests_backend/test_validation.py`). Legacy без `host` не
|
||
мигрируют. Экспорт одиночного plan/backup сохраняет `host` без ремапа id
|
||
— корректно. **Не пройдено**: см. High-1 — space merge/import ломает
|
||
ссылку `host.id` при ремапе, что прямо противоречит этому AC.
|
||
10. **AC10 (accessibility/touch).** Диалог удаления — `hp-dialog` с
|
||
доступным списком (`deleteRequiresAccessibleListDialog: true` в
|
||
smoke). Pointercancel/multi-touch код (`_stagePointerCancel`) не
|
||
менялся этим diff и уже покрывал generic drag-отмену по kind+pid.
|
||
**Пройдено, проверено чтением** (специализированного touch-смока с
|
||
реальным multi-pointer эмулятором на partition-drag не гонял — вне
|
||
моего окружения, см. «Чего не проверял»).
|
||
11. **AC11 (cache/performance).** Glow не строится в Plan-режиме, где
|
||
происходит drag partition (`glowLayerVisible` завязан на `_markup`);
|
||
fingerprint для `_lightBarriers` включает геометрию/cuts/`
|
||
transparentHostedIds`, что эквивalентно требованиям ТЗ, хотя не
|
||
дословно совпадает по списку полей. HA-only tick не трогает
|
||
`_curSpaceCfg`, значит не меняет fingerprint. **Пройдено, проверено
|
||
чтением**; отдельного performance-смока не гонял (в AC не назван,
|
||
в diff нет изменений `demo/smoke_performance*`/`performance_smoke`
|
||
файлов — см. «Чего не проверял»).
|
||
12. **AC12 (regression #173/#157).** `smoke_unified_wall_tool.mjs`,
|
||
`smoke_openwall.mjs` зелёные без изменений. `PASSAGE_FORBIDDEN_FIELDS`
|
||
контракт #157 не тронут (`validation.py`, не изменялся в этой части).
|
||
**Пройдено.**
|
||
|
||
## Что проверено и корректно
|
||
|
||
- Полный резолвер `src/partition-openings.ts` — pure, explicit host, без
|
||
nearest-wall fallback, как требует §8 ТЗ; fail-dark для `resolved: null`
|
||
на всех потребителях (`_partitionOpeningCuts`, `space-render.ts:212–218`).
|
||
- Full-depth cut, jamb returns, diagonal/1-15-100см — реализовано и
|
||
протестировано юнитами против скомпилированного (не переизобретённого)
|
||
кода.
|
||
- Delete/Undo/Redo — атомарность одним `CommandStack`-снапшотом, без
|
||
обходных путей удаления без confirmation.
|
||
- HA-состояние/actions/lock-инвариант не расширяются и не ветвятся по
|
||
host kind — соответствует `docs/SCOPE.md`.
|
||
- #185 действительно исправлен для legacy room-wall openings, воспроизводимо
|
||
(red на `origin/dev`-бандле, green на текущем).
|
||
- i18n: все шесть новых ключей есть в en и ru, без утечки английского текста
|
||
в ru.json, плейсхолдеры совпадают.
|
||
- Трейлеры: единственный класс-A коммит `9f77e3e` несёт `Issue: #132`,
|
||
`User-Visible: yes`, и в этом же коммите правки обоих changelog (проверено
|
||
`git show 9f77e3e --stat`). Документационные коммиты — `User-Visible: no`,
|
||
корректно.
|
||
- `docs/CONFIG-COMPATIBILITY.md` — новая секция `host` оформлена по
|
||
установленному в файле паттерну (сравнение с секцией #157).
|
||
- Восемь канонических документов (`CANVAS.md`, `WALL-THICKNESS.md`,
|
||
`LIGHT.md`, `SUN.md`, `UX-MODES.md`, `ARCHITECTURE.md`, `USER-GUIDE.ru.md`,
|
||
`TESTING.md`) обновлены содержательно и непротиворечиво друг другу.
|
||
- Bundle freshness: три копии (`dist/`, `custom_components/houseplan/
|
||
frontend/`, `demo/srv/assets/`) побайтово идентичны после чистой сборки.
|
||
|
||
## Чего не проверял
|
||
|
||
- **`tests_backend/test_ha_import_export.py` не выполнялся** — в этом
|
||
окружении нет `homeassistant`/`.venv-backend`; `conftest.py` молча
|
||
игнорирует `test_ha_*.py` без него (задокументированное поведение).
|
||
Именно High-1 находится в файле, который этот тест покрывает — находка
|
||
получена чтением, не прогоном; автор/CI на Linux обязаны подтвердить
|
||
фикс тем же тестом с добавленным hosted-host фикстурным случаем.
|
||
- Не гонял оставшиеся 129 из 141 browser-смоков — выбор 12 покрывает по
|
||
одному представителю на каждый AC-кластер (placement/geometry/topology/
|
||
light/HA/regression/render), не весь набор поверхностей разом.
|
||
- Не гонял `performance_smoke`/выделенные performance-профили — AC11 их не
|
||
называет по имени, diff не трогает файлы `demo/smoke_performance*.mjs`;
|
||
вывод по AC11 сделан чтением кеш-инвалидации.
|
||
- Golden: не расследовал до пикселя причину `different` на
|
||
`plan-snap-line-gaps-dark` — по превью выглядит как последствие удаления
|
||
type-agnostic opening-cut из structural snap-preview (ожидаемо для #185),
|
||
но точная причина не подтверждена построчно. Эталон не тронут в этом
|
||
diff — обновление (если diff признают корректным) идёт стандартным
|
||
`golden:accept --reviewed` на полном Linux CI артефакте, не в рамках этого
|
||
ревью. Транзиентный `error` на `openings-filled-tunnel-dark` при первом
|
||
прогоне не воспроизвёлся при повторном — похоже на конкуренцию ресурсов
|
||
от параллельных фоновых процессов на этой машине, не расследовал глубже.
|
||
- Не гонял `smoke_opening_measure.mjs` — по `AGENTS.md` он уже нестабилен на
|
||
этом Chromium независимо от этой задачи (`known environment-sensitive
|
||
smoke`), диагностический шум не относится к #132.
|
||
- Composite (coincident partition + room-wall) double-cut подтверждён только
|
||
чтением кода и косвенно golden-сценами; отдельного unit/smoke на именно
|
||
этот сценарий нет (зафиксировано выше при разборе AC2, не заведено
|
||
отдельным issue — граница между «стоит добавить» и «доказано чтением»
|
||
решена в пользу второго, так как код однозначен).
|
||
- Touch-специфичный multi-pointer сценарий для partition-drag — проверен
|
||
чтением generic `_stagePointerCancel`, не эмулировал реальный
|
||
multi-touch в браузере.
|
||
|
||
## Вердикт
|
||
|
||
Красный. High: 1, Medium: 3 (заведены #186, #187, #188). AC9 не выполнен
|
||
из-за High-1: `build_space_merge()` не ремапит `opening.host.id` при смене
|
||
id partition, из-за чего экспорт/импорт (дублирование) любого space с
|
||
partition-hosted проёмом безусловно отклоняется backend-валидацией. Это
|
||
блокирующий дефект по прямо заявленному в ТЗ AC, а не крайний случай.
|
||
Остальные 11 AC пройдены (частично — с проверкой чтением там, где отдельного
|
||
теста нет, см. разбор по AC и «Чего не проверял»); дешёвые гейты и
|
||
целевые смоки/golden зелёные. После исправления High-1 (ремап
|
||
`opening.host.id` через тот же `id_map`, что и остальные ссылки в
|
||
`build_space_merge`) и добавления регрессионного теста в
|
||
`test_ha_import_export.py`, ожидаю зелёный вердикт без повторного разбора
|
||
остальных AC — они не затронуты фиксом.
|