mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности (`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` → `legacy/specs/`), а относительные ссылки внутри перенесённых документов и в соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно только до переноса документов ревью). Гейты архив не смотрят. `reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не резолвится от нового места, а цель находится от нового или старого места через карту переносов; битая и до переноса ссылка не трогается. `--apply` делает это само, `--repair-links=<rev>` — для всех переименований `<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом `--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты: перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет; мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и DEVELOPMENT › Release называют переписывание и `--check-links`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
349 lines
32 KiB
Markdown
349 lines
32 KiB
Markdown
# 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 → в задаче
|