mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,348 @@
|
||||
# CODE-REVIEW-316-r1 — миграция v9 сама разрешает конфликт «проём ↔ нулевая стена»
|
||||
|
||||
- Issue: [#316](https://github.com/Matysh/houseplan-card/issues/316)
|
||||
- Этап: `code` (PROCESS.md §2.7)
|
||||
- Диапазон: `origin/dev...HEAD`, `origin/dev` = `f3fe8637` (docs: review document for #319),
|
||||
`HEAD` = `ee672dcd` (ветка `issue/316-migration-auto-resolve`, ребейз на `dev` выполнен
|
||||
автором до этого ревью — коммент issue от 2026-08-26T19:12:24Z, конфликт был только
|
||||
в `CHANGELOG`)
|
||||
- ТЗ: [`docs/specs/316-opening-host-auto-resolution.md`](../specs/316-opening-host-auto-resolution.md),
|
||||
ревизия 4, ревью ТЗ зелёное — [`SPEC-REVIEW-316-r4.md`](SPEC-REVIEW-316-r4.md)
|
||||
- Заход ревью: **r1**, блокирующих циклов код-ревью израсходовано **0/4**
|
||||
- Вердикт: **красный**
|
||||
|
||||
## Скоуп ревью
|
||||
|
||||
Первый заход код-ревью (ревью ТЗ бюджет не тратит, §10.4/§227; первая попытка
|
||||
запуска код-ревью не состоялась из-за конфликта ребейза с `dev` и цикл не
|
||||
израсходовала — это не первый *заход* по номеру документа, но первый
|
||||
фактический разбор кода). Полный разбор, дельты по §2.10 нет.
|
||||
|
||||
Продуктовый код (класс A): `src/wall-segment-model.ts` (+111/−7),
|
||||
`custom_components/houseplan/wall_segment_model.py` (Python-зеркало миграции,
|
||||
+96/−9), `custom_components/houseplan/validation.py` (+4/−1, разрешение
|
||||
`host: null` в `_config_wall_segment_invariants`).
|
||||
|
||||
Гейты/инструменты (класс B): `test/wall-segment-model.test.mjs` (+97/−7,
|
||||
6 новых тестов), `tests_backend/test_wall_segment_model.py` (+51/−10, 4 новых
|
||||
теста), `demo/smoke_zero_wall_migration_unblocked.mjs` (новый),
|
||||
`demo/golden/{harness,matrix}.mjs`, `demo/golden/baselines/baselines-index.json`
|
||||
(новый эталон `span-over-door-migrated-dark`, matrixVersion 46→47),
|
||||
`test/golden-matrix.test.mjs` (версия матрицы), `test/fixtures/316-span-over-door-migrated.json`
|
||||
(новый), `scripts/config-field-registry.mjs` (запись `host=wall` переписана),
|
||||
`scripts/mutation-gate.mjs` (2 новых мутанта).
|
||||
|
||||
Документация (класс C): `docs/CONFIG-COMPATIBILITY.md` (раздел «Canonical
|
||||
zero-thickness walls — model v9» переписан под §3.1–3.4), `docs/CHANGELOG.md` +
|
||||
`docs/CHANGELOG.ru.md` (`User-Visible: yes`, оба в одном коммите).
|
||||
|
||||
Сгенерированное (класс D): `dist/houseplan-card.js`,
|
||||
`custom_components/houseplan/frontend/houseplan-card.js` (сверены байт-в-байт с
|
||||
локальной пересборкой), `demo/golden/baselines/span-over-door-migrated-dark.png`
|
||||
(коммит несёт `Release:`/`Baseline-Reviewed:` — прогон CI 33000824647).
|
||||
|
||||
i18n не тронут (`src/i18n/**` вне диффа) — соответствует ТЗ §5.2 («без изменений»).
|
||||
|
||||
Прочитано до вердикта: `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md`, тело issue
|
||||
#316 и все 12 комментариев (аналитика, все 4 раунда ревью ТЗ, хендофф
|
||||
реализации, отчёт о несостоявшемся первом запуске код-ревью из-за конфликта
|
||||
ребейза, отчёт о ребейзе), ТЗ ревизии 4 целиком, `docs/CONFIG-COMPATIBILITY.md`
|
||||
(разделы «Canonical zero-thickness walls» и «Independent-wall opening host»),
|
||||
весь новый/изменённый продуктовый код в TS и Python, все новые тесты
|
||||
(frontend + backend), новый смок, изменения `config-field-registry.mjs` и
|
||||
`mutation-gate.mjs`.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| Типы | `npx tsc --noEmit` | green |
|
||||
| Unit (frontend) | `npm test` | **1351/1352** green, 1 skipped (не относится к диффу — существующий known-skip) |
|
||||
| Сборка | `npm run build` | green |
|
||||
| Синхронность бандла | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | идентичны байт-в-байт |
|
||||
| Документация (обязателен: diff трогает `src/**`) | `node scripts/check-docs.mjs` | green — «Documentation checks passed (7 files, 10 external links)» |
|
||||
| Инварианты модели (диф трогает `host`/атомизацию) | `npm run invariants -- --config <фикстура AC1>` и `--config <мой репро §H1>` | оба «Инварианты выполнены» — эта проверка **не видит** находку H1 (см. ниже, она проверяет разрешимость ссылок и наличие записи толщины, не геометрическое согласие host↔x/y) |
|
||||
| Backend pytest | `python -m pytest tests_backend -q` | **недоступно в этой среде** — `homeassistant` не установлен (нет `.venv-backend`, чистый контейнер ревью, не cloud-агент). Компенсировано прямым исполнением: модули `validation.py`/`wall_segment_model.py` загружены в изоляции от `custom_components/houseplan/__init__.py` (который требует HA) через `importlib` с застабленным пакетом — это и вскрыло находку H1 (ниже, точный скрипт воспроизведения приложен) |
|
||||
| Смоки — по выборке `smoke-select.mjs` (прямое совпадение) | `node demo/smoke_edit_walk.mjs`, `smoke_glow.mjs`, `smoke_junction_holes.mjs` | все **OK** |
|
||||
| Смоки — зарегистрированная связь | `smoke_real_plan_masonry.mjs`, `smoke_resize_pointer_real_plan.mjs`, `smoke_resize_wall_thickness.mjs` | все **OK** (связь подтверждена, регрессий нет) |
|
||||
| Смок AC1 (репродукция #316) | `node demo/smoke_zero_wall_migration_unblocked.mjs` | **OK** — но см. H1: этот смок мокает `callWS` и никогда не пропускает мигрированный кандидат через реальную бэкенд-схему, поэтому находку не ловит |
|
||||
| Golden | `npm run bundle:sync && npm run golden:verify` (полный матрикс, 130+ сцен, включая `span-over-door-migrated-dark` и `coincident-partition-virtual-dark`) | **все `passed`**, exit 0 |
|
||||
| single-source-numbers | `node --test test/single-source-numbers.test.mjs` | green (3/3), не по существу диффа, но диф трогает видимую геометрию — прогнан для очистки |
|
||||
| Дисциплина «тест умеет падать» | вручную применены оба мутанта `mutation-gate.mjs` (§3.1: `span-cut-erases-the-door-wall`, §3.4: `migration-throws-over-an-opening-again`) на `src/wall-segment-model.ts`, пересобран `test-build`, целевой тест перезапущен | **оба красные** — тесты AC2/AC1 реально способны упасть (полный вывод в разделе «Проверка AC») |
|
||||
|
||||
**Одно число — один источник.** В диффе нет новой пользовательски видимой
|
||||
величины (числа, подписи) — только геометрия (позиции атомов/host). Проверка
|
||||
не применима буквально к «числу», но её геометрический аналог — согласие
|
||||
`opening.host` с фактической позицией `opening.x/y` — и есть предмет находки
|
||||
H1: после миграции у проёма фактически **два источника позиции**, которые
|
||||
могут разойтись (сохранённые `x/y` и позиция, которую подразумевает
|
||||
`host.id + host.t`), и этот разлад не смягчается рендером (он честно рисует по
|
||||
`x/y`), а ломается ниже по цепочке — на записи.
|
||||
|
||||
## Находки
|
||||
|
||||
### H1 (High, в скоупе) — деградированный host (§3.3) может завести данные, которые бэкенд тут же отклоняет: миграция снова блокирует запись
|
||||
|
||||
**Смысл.** `migrateRoomOpeningHost` (`src/wall-segment-model.ts:679-708`, зеркало
|
||||
`_host_openings`/`degraded` в `custom_components/houseplan/wall_segment_model.py:580-612`)
|
||||
для деградированного случая (§3.3) выбирает *любую* стену пространства,
|
||||
подходящую по углу (`wallAngleMatches`, допуск 8°) и вместимости (`length`),
|
||||
**без ограничения по расстоянию** — буквально по тексту ТЗ §3.3. Дальше host
|
||||
материализуется как `{kind:'wall', id, t: clamp(projectT(centre, host.a, host.b))}`
|
||||
— `t` берётся из ортогональной проекции центра проёма на прямую хоста, а не из
|
||||
факта, что проём на ней лежит.
|
||||
|
||||
Но `opening.x/y` при этом **не меняются** — миграция их не трогает нигде.
|
||||
|
||||
Если после этого документ проходит через `CONFIG_SCHEMA` (а он проходит:
|
||||
`config/set` и `plan/optimize` в `websocket_api.py` валидируют кандидат **после**
|
||||
`commit_wall_segment_model`, строки 1653/1661-1662 и 1325 применительно к уже
|
||||
мигрированному фронтендом кандидату), срабатывает существующая, не тронутая
|
||||
этим диффом проверка `_config_wall_segment_invariants`
|
||||
(`custom_components/houseplan/validation.py:1849-1853`): она пересчитывает
|
||||
`x, y` из `host.a/b + t` и требует `abs(x - opening.x) ≤ 2e-8`. Для
|
||||
деградированного host (по построению *не* близкого к проёму — вся суть §3.3 в
|
||||
том, что рядом ничего нет) это расхождение равно перпендикулярному расстоянию
|
||||
от проёма до выбранной стены — на практике многие сантиметры в нормализованных
|
||||
координатах, на много порядков больше допуска. Проверка падает с
|
||||
`wall opening geometry must match its host`, `CONFIG_SCHEMA` кидает
|
||||
`vol.Invalid`, и `websocket_api.py` (оба пути, `config/set:1339-1341` и
|
||||
`plan/optimize:1687-1688`) отвечает клиенту `invalid_format` — **запись
|
||||
отклонена целиком**.
|
||||
|
||||
Итог: для этого класса входных данных миграция не выполняет своё собственное
|
||||
нормативное правило §3.4 («initial migration никогда не кидает opening-host»)
|
||||
по факту — она действительно не кидает `WallSegmentModelError('opening-host')`
|
||||
из `hostRoomOpenings`, но провоцирует эквивалентный по последствиям отказ
|
||||
чуть ниже по конвейеру, тем же классом ошибки («пространство не
|
||||
преобразовано»), который и есть предмет #316. Часть, ради которой заведён
|
||||
issue, — «рисование нигде не блокируется» (тело ТЗ, §1) — не выполняется для
|
||||
этого случая.
|
||||
|
||||
**Заодно замечание по замыслу.** Присвоенный деградированный `host` не даёт
|
||||
никакой функциональной пользы: рендер контурных host'ов всегда идёт по
|
||||
собственным `x/y` проёма (`space-render.ts:285`,
|
||||
`plan-geometry-preflight.ts:224/276`, `houseplan-card.ts:9282`), а
|
||||
физика/вырезы (`physical-geometry.ts`) режут тело по геометрическому
|
||||
пересечению, не по `host`. Единственные потребители `host.id` для
|
||||
`kind:'wall'` — это (а) сравнение линии владения при протяжке ID-наследования
|
||||
(`openingHostCounts`, `wall-segment-model.ts:296-303`) и (б) блокировка
|
||||
инструмента «Толщина» при занулении стены, которая якобы несёт проём
|
||||
(`houseplan-card.ts:11753-11758`, не тронут этим диффом). Для (б) деградированный
|
||||
host активно вреден: он запрещает занулить чужую, физически не связанную с
|
||||
проёмом стену, и одновременно снимает позиционную защиту (`if (opening.host) return false;`
|
||||
на той же строке 11756) с настоящей стены под проёмом, у которой `host` не установлен.
|
||||
То есть даже если бы бэкенд-валидацию поправили, деградированный host остаётся
|
||||
данными без пользы и с прямым вредом для UX инструмента «Толщина» —
|
||||
это довод в пользу того, что §3.3 в этой части должен вести сразу к
|
||||
непривязанному состоянию (без host), а не к «дальней» привязке; ТЗ прямо
|
||||
допускает такую альтернативу («альтернатива «сразу непривязанный без поиска»
|
||||
тоже согласуется с решением владельца», §6 ТЗ).
|
||||
|
||||
**Почему это не поймали шлюзы.** Ни один прогнанный гейт не проверяет
|
||||
согласие `host`↔`x/y` после миграции:
|
||||
- `npm test` не вызывает `CONFIG_SCHEMA` вообще (чистый TS, backend не участвует);
|
||||
- `tests_backend/test_wall_segment_model.py::test_unhosted_contour_opening_is_a_valid_degraded_v9_state`
|
||||
— единственный тест, который пропускает мигрированный кандидат через
|
||||
`CONFIG_SCHEMA` (строка добавлена этим диффом), но его фикстура (`angle: 90`
|
||||
против единственной стены `angle: 0`) не даёт **ни одного** кандидата даже в
|
||||
деградированном пуле — угол не совпадает вовсе, поэтому проём уходит в
|
||||
истинно непривязанное состояние (`host` отсутствует) и находка не
|
||||
воспроизводится;
|
||||
- `demo/smoke_zero_wall_migration_unblocked.mjs` мокает `card.hass.callWS`
|
||||
безусловным `{ ok: true }` — реальная бэкенд-схема не участвует;
|
||||
- `npm run invariants` (`scripts/model-invariants.mjs`) проверяет
|
||||
разрешимость ссылок и наличие записи толщины по грид-ключу (#254/#258/#259),
|
||||
но не геометрическое согласие `host.t`↔`x/y` — проверено запуском на моём
|
||||
воспроизведении (см. ниже), инварианты «выполнены», находку не видят;
|
||||
- golden/визуальные гейты не видят разницы, т.к. рендер идёт по `x/y`
|
||||
независимо от `host` — визуально деградированный и истинно непривязанный
|
||||
случай неотличимы.
|
||||
|
||||
**Воспроизведение (проверено исполнением, дважды — TS через реальный `dist`-путь
|
||||
модуля и Python-зеркало через прямую загрузку модулей в обход `custom_components/houseplan/__init__.py`,
|
||||
результат идентичен):**
|
||||
|
||||
```js
|
||||
// TS, тот же код, что использует фронтенд (test-build/wall-segment-model.js)
|
||||
const config = {
|
||||
spaces: [{
|
||||
id: 'floor', title: 'Floor', cell_cm: 5, view_box: [0,0,1,1],
|
||||
rooms: [{ id: 'room', poly: [[0,0],[1,0],[1,1],[0,1]] }],
|
||||
walls: [{ key: '', a: [0,0], b: [1,0], cm: 15 }],
|
||||
openings: [{ id: 'orphan', type: 'door', x: 0.5, y: 0.5, angle: 0, length: 0.1 }],
|
||||
}],
|
||||
markers: [], settings: {},
|
||||
};
|
||||
commitWallSegmentModel(config).config.spaces[0].openings[0];
|
||||
// → { id:'orphan', type:'door', x:0.5, y:0.5, angle:0, length:0.1,
|
||||
// host:{ kind:'wall', id:'wall-6jl52lapgi6hgvc2qe6r', t:0.5 } }
|
||||
// opening.x/y (0.5, 0.5) остаются исходными; host ссылается на стену y=0 —
|
||||
// перпендикулярное расстояние 0.5, т.е. половина плана.
|
||||
```
|
||||
|
||||
```python
|
||||
# Python-зеркало, идентичный вход
|
||||
migrated, _ = commit_wall_segment_model(src)
|
||||
# migrated["spaces"][0]["openings"][0]["host"] ==
|
||||
# {"kind": "wall", "id": "wall-6jl52lapgi6hgvc2qe6r", "t": 0.5}
|
||||
CONFIG_SCHEMA(copy.deepcopy(migrated))
|
||||
# → voluptuous.error.MultipleInvalid: wall opening geometry must match its host
|
||||
```
|
||||
|
||||
Команды и полный стек трассировки воспроизведены локально в рамках этого
|
||||
ревью (см. «Как проверялось» — раздел backend pytest недоступен, но модули
|
||||
загружены напрямую тем же интерпретатором и той же версией `voluptuous`, что
|
||||
использует `tests_backend`).
|
||||
|
||||
**Требуется:** до возврата на правку — не мой мандат чинить, но по существу:
|
||||
деградированный кандидат в §3.3 обязан либо (а) материализовывать `host` только
|
||||
когда реконструированные `x/y` совпадают с сохранёнными в пределах допуска,
|
||||
который реально примет `_config_wall_segment_invariants` (т.е. фактически
|
||||
эквивалентно текущему `eligible()`, а не отдельному безлимитному пулу), либо
|
||||
(б) уходить сразу в истинно непривязанное состояние без `host`, как это уже
|
||||
происходит, когда угол не совпадает вовсе. Второй вариант дешевле и совпадает
|
||||
с уже одобренной в ТЗ §6 альтернативой.
|
||||
|
||||
### M1 (Medium, в скоупе) — CHANGELOG заявляет то, чего код не гарантирует
|
||||
|
||||
`docs/CHANGELOG.md`/`.ru.md`: «Drawing works again everywhere: converting a
|
||||
plan to the new wall model no longer stops on a legacy border/opening
|
||||
conflict» / «Рисование снова работает везде… больше не останавливается». При
|
||||
находке H1 это утверждение ложно для деградированного случая — превращение
|
||||
всё ещё останавливается, просто с другим кодом ошибки на другом уровне. Формулировка
|
||||
чинится в этом же коммите вместе с H1 (её объём и формулировка зависят от
|
||||
выбранного решения H1), отдельного issue не заводится (Medium в скоупе, #202).
|
||||
|
||||
## Проверка AC
|
||||
|
||||
- **AC1 (репродукция #316).** Доказательство — смок
|
||||
`demo/smoke_zero_wall_migration_unblocked.mjs`, воспроизводящий ровно
|
||||
сценарий issue (span+door в одном пространстве, пустое — в другом). Прогнан:
|
||||
**OK**. Мутант `migration-throws-over-an-opening-again` делает его красным
|
||||
при откате исправления — проверено вручную (раздел «Как проверялось»).
|
||||
AC доказан для **этого конкретного** сценария (§3.1, не §3.3) — см. H1 про
|
||||
общий контракт «никогда не блокирует».
|
||||
- **AC2 (§3.1).** Доказательство — юнит-тест
|
||||
`an opening keeps its wall through a legacy span cut`. Прогнан: **OK**;
|
||||
мутант `span-cut-erases-the-door-wall` делает его красным при откате —
|
||||
проверено вручную, тест реально падает (`AssertionError: the atom under the
|
||||
door keeps its real thickness`). AC доказан.
|
||||
- **AC3 (§3.2).** Доказательство — юнит-тест
|
||||
`an ambiguous carrier is resolved deterministically at a thickness boundary`.
|
||||
Фикстура — стык двух контурных атомов одной стены на границе смены толщины,
|
||||
проём центрирован ровно на стыке (перпендикулярное расстояние 0 к обоим
|
||||
кандидатам) — реально достижима внутри `resolveRoomOpeningHost`, тай-брейк
|
||||
«больший cm» проверяется явно. AC доказан. Тай-брейк «текущий host» и
|
||||
«меньший id» кодом реализован (`pick()`, TS и Python идентичны построчно),
|
||||
но отдельными тестами не покрыт — не блокирует (не входит в AC3 буквально),
|
||||
отмечаю как Low-полноту тестового покрытия, не требую правки.
|
||||
- **AC4 (§3.3).** Доказательство — юнит-тест фронта
|
||||
(`an opening with no usable carrier migrates unhosted...`) + бэкенд-тест
|
||||
схемы (`test_unhosted_contour_opening_is_a_valid_degraded_v9_state`). Оба
|
||||
зелёные, но оба тестируют только **истинно пустой** пул кандидатов (угол
|
||||
вообще не совпадает ни с одной стеной) — не тестируют деградированный пул с
|
||||
найденным, но геометрически несвязанным кандидатом, который и оказался
|
||||
сломан (H1). Формально буква AC4 («проём вдали от всех стен мигрирует в
|
||||
состояние «без host»») для протестированного случая выполняется; для
|
||||
непротестированного соседнего случая, нормативно предписанного тем же §3.3,
|
||||
— нет. **AC не доказан полностью.**
|
||||
- **AC5 (граница §2).** Доказательство — юнит-тест
|
||||
`a post-v9 write that LOST its carrier keeps the fail-closed refusal`
|
||||
(frontend) + `test_post_v9_write_that_lost_its_carrier_keeps_the_refusal`
|
||||
(backend). Оба зелёные, оба явно проверяют `WallSegmentModelError`/
|
||||
`WallSegmentMigrationError('opening-host')` при `initialMigration=false`.
|
||||
AC доказан.
|
||||
- **AC6 (идемпотентность).** Доказательство — `commit(commit(x)) === commit(x)`
|
||||
байтово, проверено в каждом новом тесте (§3.1/§3.2/§3.3/фикстура) и отдельно
|
||||
тестом на зафиксированной фикстуре
|
||||
`316-span-over-door-migrated.json`. Проверено запуском (`node --test`),
|
||||
AC доказан для протестированных путей. Не проверяет: идемпотентность
|
||||
сохраняется и для деградированного host из H1 (сам факт того, что
|
||||
`commit(x)` неконсистентен с `CONFIG_SCHEMA`, делает вопрос об идемпотентности
|
||||
вторичным — до записи дело не доходит).
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- §3.1 (легаси-кат щадит атом с проёмом) реализован идентично в TS и Python:
|
||||
один и тот же критерий (`GRID_STEP_N*0.02` по расстоянию, тот же угловой
|
||||
критерий), фильтр «проём реально стоит на легаси-кате»
|
||||
(`GRID_STEP_N*0.04`) не даёт постороннему проёму перекраивать каталог —
|
||||
проверено чтением и тестом на фикстуре с «дальним» проёмом (из хендоффа
|
||||
автора о фиксе после ребейза).
|
||||
- §3.2 тай-брейк (текущий host → расстояние → cm → id) реализован одинаково в
|
||||
`pick()` TS и Python, порядок сравнений совпадает построчно.
|
||||
- §3.4 (initial migration никогда не throw из `hostRoomOpenings`) — верно
|
||||
локально для этой функции; связка с H1 показывает, что гарантия не
|
||||
переживает следующий слой (`CONFIG_SCHEMA`), но именно эта функция сама по
|
||||
себе корректна.
|
||||
- `scripts/config-field-registry.mjs`, `docs/CONFIG-COMPATIBILITY.md`
|
||||
(раздел «Canonical zero-thickness walls — model v9») правлены в том же
|
||||
коммите, что и код (правило 11), формулировки соответствуют §3.1–3.4 ТЗ.
|
||||
Раздел «Independent-wall opening host (#132)» не тронут — верно, партиционные
|
||||
проёмы вне скоупа (`hostRoomOpenings` явно пропускает `kind==='partition'`,
|
||||
не изменено этим диффом).
|
||||
- Golden: новая сцена `span-over-door-migrated-dark` добавлена с корректным
|
||||
`Release:`/`Baseline-Reviewed:` в отдельном D-коммите; полный прогон
|
||||
`golden:verify` (130+ сцен, включая эту и `coincident-partition-virtual-dark`)
|
||||
— все `passed`, подтверждает заявление ТЗ §5.3 «без изменений» для
|
||||
партиционной сцены.
|
||||
- Трейлеры коммитов корректны: `Issue: #316` на обоих (реализация и
|
||||
D-коммит), `User-Visible: yes` на реализации с правками в обоих CHANGELOG в
|
||||
том же коммите, `User-Visible: no` + `Release:`/`Baseline-Reviewed:` на
|
||||
golden-коммите. `pre-push`/`process-gate` не могли это поймать сами —
|
||||
подтверждено чтением коммитов, не только заявлением автора.
|
||||
- i18n действительно не тронут (`git diff --stat` по `src/i18n/**` пуст),
|
||||
соответствует ТЗ §5.2.
|
||||
- Ребейз на `dev` (после несостоявшегося первого запуска ревью) не принёс
|
||||
нового кода — единственный конфликт был в `CHANGELOG.md`/`.ru.md` с уже
|
||||
слитым #319; проверено чтением коммита `19:12:24Z` и тем, что
|
||||
`wall-segment-model.ts`/`.py` не менялись между `df893c1c` (отвергнутый
|
||||
первый HEAD) и `ee672dcd` за вычетом уточнения edge-breaks, которое автор сам
|
||||
описал и которое покрыто существующими тестами на «посторонний проём не
|
||||
перекраивает каталог».
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- **`python -m pytest tests_backend -q` целиком** — недоступен в этой среде
|
||||
(нет `homeassistant`, нет `.venv-backend`). Компенсация: прямой запуск
|
||||
`validation.py`/`wall_segment_model.py` в обход пакета — тем же способом
|
||||
подтверждены оба заявленных автором числа тестов существованием функций и
|
||||
найдена H1; полный набор (18/19 по заявлению автора) не прогнан этим
|
||||
ревью буквально, доверия к зелёному CI (`Baseline-Reviewed` ссылка на прогон
|
||||
33000824647/33001442307) достаточно для остальных backend-тестов, кроме
|
||||
специфического сценария H1, которого в наборе нет.
|
||||
- **Полный набор `demo/smoke_*.mjs` (192 файла)** — не прогнан целиком, только
|
||||
выборка `smoke-select.mjs` (прямое совпадение + зарегистрированная связь) и
|
||||
AC1-смок. Диф не задевает подсистемы, для которых нужен полный прогон
|
||||
(партиции, изометрия, устройства); полный прогон — обязанность предрелизного
|
||||
гейта (PROCESS.md §8).
|
||||
- **`performance_smoke`** — не запускал; ни AC, ни диф не называют влияние на
|
||||
перф (ТЗ §5.4 явно: миграция — единственный проход по проёмам, вне горячего
|
||||
пути рендера), и диф не трогает рендер-цикл.
|
||||
- **`npm run invariants` на реальном экспорте с продакшен-плана** — прогнан
|
||||
только на синтетических фикстурах (AC1 golden-фикстура и моё воспроизведение
|
||||
H1); ни то ни другое не эквивалент «реального большого плана», но
|
||||
`smoke_real_plan_masonry.mjs` (косвенно, через связку с `atomicPolyForRoom`)
|
||||
прогнан и зелёный.
|
||||
- **Тай-брейк «текущий host» и «меньший id» в §3.2** — реализация проверена
|
||||
чтением (идентична TS/Python), отдельным юнит-тестом не покрыта; не поднимаю
|
||||
до Medium, т.к. это не AC3 буквально и код прямолинеен (простая сортировка).
|
||||
|
||||
## Резюме
|
||||
|
||||
High-находка H1 показывает, что нормативное правило §3.4 («initial migration
|
||||
никогда не блокирует запись из-за проёма») не выполняется целиком: для
|
||||
деградированного случая §3.3 миграция создаёт геометрически несогласованный
|
||||
`host`, который существующая (не тронутая этим диффом) бэкенд-валидация
|
||||
`_config_wall_segment_invariants` отклоняет — воспроизведено исполнением и в
|
||||
TS, и в Python. Ни один из прогнанных гейтов (unit, смоки, golden,
|
||||
model-invariants) этот путь не покрывает; единственный тест, который **мог**
|
||||
бы его поймать (`test_unhosted_contour_opening_is_a_valid_degraded_v9_state`),
|
||||
случайно обходит деградированный пул стороной из-за несовпадения угла в своей
|
||||
фикстуре. Возврат автору для исправления материализации host в §3.3 (или
|
||||
замены на прямой переход к непривязанному состоянию) и правки формулировки
|
||||
CHANGELOG (M1) следом.
|
||||
|
||||
Вердикт: **красный** · заход r1 · блокирующих циклов код-ревью 1/4 · High: 1 ·
|
||||
Medium: 1 → в задаче
|
||||
Reference in New Issue
Block a user