Files
houseplan-card/docs/reviews/SPEC-REVIEW-529-r2.md
T
2026-09-11 08:13:43 +00:00

91 lines
16 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.
# SPEC-REVIEW-529-r2
**Issue:** #529 — «Тупик «conflicting wall identifiers»: план правится только после ручной чистки, а совет «Optimize plans» не помогает (по #527)»
**Этап:** spec (ревью ТЗ, PROCESS.md §2.4)
**Трек:** полный (заявленный автором критерий: «нет миграции конфига и новых compatibility-полей» — не пройден)
**Заход:** r2 · блокирующих циклов израсходовано 1 из 4 (жёлтый вердикт r1 цикл потратил; зелёный вердикт этого раунда бюджет не тратит, #227)
## Скоуп раунда
По PROCESS.md §2.10 предмет второго раунда — дельта тела issue между r1 и текущим состоянием, а не ТЗ целиком. r1 (`docs/reviews/SPEC-REVIEW-529-r1.md`, материал: ветка `dev`, коммит `e408b6fb70a9`, дерево `0885d3e45b9242464d2fadc048fb64ce54c65a74`) вынес жёлтый вердикт с единственной находкой — Medium в скоупе, К4/AC5. Автор ответил комментарием от 2026-09-11T08:08:11Z, перечислив пять правок текста. Дельта локальна (правка одного контрактного пункта и одного AC внутри уже описанной подсистемы, без новой подсистемы и без ребейза — здесь просто нет кода и веток), поэтому объём разбора сокращается до дельты плюс всего, чего она касается:
1. К4 переписан на реальный механизм самолечения;
2. добавлен К4а — про третью точку защиты (`_config_wall_segment_invariants`);
3. AC5 переформулирован — больше не утверждает несуществующую последовательность вызовов;
4. добавлен AC5а — свидетель клиентского пути лечения;
5. пункт 2 «Принятых технических предположений» и раздел «Затронутые файлы» приведены в соответствие с реальным кодом `ws_config_set`.
Всё остальное (К1, К2, К3, К5, К6, AC1-AC4, AC6-AC8, продуктовая рамка §7.1, i18n, откат, риски, миграция, производительность, touch, пункты 1 и 3 «предположений») дельта не задевает — унаследовано из r1 без повторной проверки (раздел ниже).
## Как проверялось
Чтение кода, без исполнения. На этапе ТЗ продуктового кода нет: `git branch -a` не содержит `issue/529-*`, `git log --all --oneline --grep="#529"` не находит ни одного коммита класса A/B — только собственный коммит документа r1. Гейты (`tsc`, `npm test`, `npm run build`, pytest) неприменимы: предмет ревью текст ТЗ, не код.
Перечитано заново, по каждому пункту дельты:
- `custom_components/houseplan/validation.py:1893-1907` (`_config_wall_segment_invariants`) — подтверждена точная формулировка, которую К4а теперь называет по имени и номеру строки: `if model >= 10 and "room_drafts" in space: raise vol.Invalid("v10 config must not contain room_drafts")` — безусловно, без разбора длины `room_drafts`.
- `custom_components/houseplan/websocket_api.py:1595-1666` (`ws_config_set`, вложенная `_normalize`) — подтверждена последовательность `validate_wall_model_transition(candidate, previous)` → `CONFIG_SCHEMA(candidate)`, миграции (`commit_wall_segment_model`) в этом обработчике нет вовсе — ровно то, что теперь пишет пункт 2 «предположений».
- `custom_components/houseplan/validation.py:142-183` (`validate_wall_model_transition`) — текущее условие `old_model >= 10 and new_model < 10 and any(...)`, которое К3 (не изменён этим раундом) переписывает на «есть в заявке, нет в сохранённом».
- `src/wall-segment-model.ts:741-802` (`migrateRoomDraftsToPartitions`) — подтверждено: `if (!Object.prototype.hasOwnProperty.call(space,'room_drafts')) return`; далее `if (!initialMigration) throw` безусловно, без разбора пустого/непустого списка; в успешной ветке — `delete space.room_drafts`.
- `src/wall-segment-model.ts:825-832` (`migrateSpace`) и `:861-880` (`commitWallSegmentModel`) — подтверждено: `commitWallSegmentModel` зовёт `migrateSpace` → `migrateRoomDraftsToPartitions(space, initialMigration)` для **каждого** пространства конфига, независимо от `initialMigration`. Значит после снятия безусловного `raise` (К1/К2 — предмет этого ТЗ, не этого раунда) результат `commitWallSegmentModel` на конфиге с `room_drafts` (пустым или нет) гарантированно не будет содержать этот ключ — то есть именно то, что обещает AC5а.
- `src/houseplan-editor-runtime.ts:1999-2037, 2205-2250, 2395-2410` и `src/draft-live-commit.ts:61-90` — проверены все пять цитат К4 (`:2005`, `:2030`, `:2247`, `:2405`, `draft-live-commit.ts:79`): каждая — реальный вызов `commitWallSegmentModel` на клиентском кандидате конфига, до того как этот кандидат уходит в `_saveConfig`/`config/set`.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| Medium: К4/AC5 описывали самолечение через последовательность `validate_wall_model_transition` → `commit_wall_segment_model`, которой в `ws_config_set` не существует; третья точка защиты (`_config_wall_segment_invariants`) не была названа | К4 переписан: механизм — клиентское зеркало `commitWallSegmentModel` (`houseplan-editor-runtime.ts`, `draft-live-commit.ts`), а не серверная миграция; добавлен К4а, называющий `_config_wall_segment_invariants` (`validation.py:1906`) по имени и описывающий её сохранённое (непустой ключ) / снятое (пустой ключ) поведение; AC5 переформулирован на две независимые, реально достижимые проверки (сторож не бросает + схема принимает пустое/отвергает непустое); добавлен AC5а, проверяющий именно клиентский путь лечения | Тело issue #529, разделы «Контракт» (К4, К4а) и «AC» (AC5, AC5а); подтверждено чтением кода — см. «Как проверялось» выше |
| (то же расхождение) пункт 2 «Принятых технических предположений» описывал порядок «валидация → миграция», которого для `config/set` нет | Переписан на «сторож переходов → `CONFIG_SCHEMA`; миграции там нет, она живёт на путях оптимизации, импорта и экспорта» | Тело issue, «Принятые технические предположения», пункт 2 — совпадает с фактическим кодом `ws_config_set._normalize` |
| (то же расхождение) «Затронутые файлы» не называли `_config_wall_segment_invariants` | `validation.py` теперь назван с явной оговоркой «и сторож переходов, и инвариант схемы `_config_wall_segment_invariants`» | Тело issue, раздел «Затронутые файлы» |
## Унаследовано из r1
Принято без повторной проверки в этом раунде, по документу `docs/reviews/SPEC-REVIEW-529-r1.md` (материал: ветка `dev`, коммит `e408b6fb70a9`, дерево `0885d3e45b9242464d2fadc048fb64ce54c65a74`):
- Продуктовая рамка §7.1 (персона — Home admin, поверхность — редактор плана, момент — создание/переименование комнаты или экспорт; «до/после» без терминов реализации).
- К1, К2 — корректно описывают текущий баг: `_migrate_room_drafts_to_partitions`/`migrateRoomDraftsToPartitions` роняют коммит безусловно при `!initial_migration`, без разбора пустого/непустого `room_drafts`.
- К3 — корректно описывает промах сторожа устаревшего клиента (`old_model>=10 and new_model<10`, тогда как карточка эхом отправляет полученный `model_version=10`).
- К5/AC6 — `import_export._create_export` зовёт `commit_wall_segment_model` до `CONFIG_SCHEMA`; починка К1/К2 закрывает и экспорт без отдельной правки `import_export`.
- К6/AC3 — паритетная фикстура `test/fixtures/282-wall-identity-parity.json` и `test/wall-segment-model.test.mjs:48` существуют; Python- и TS-зеркала содержат симметричный баг.
- AC1, AC2, AC4, AC7, AC8 — сформулированы однозначно, со способом доказательства, дельтой не изменены.
- i18n — `toast.wall_model_client_outdated` уже существует в `src/i18n/en.json:743` и `ru.json:743`; новых ключей не требуется.
- Откат, риски, миграция/совместимость, производительность, touch — без зазоров.
- Пункты 1 и 3 «Принятых технических предположений» — не изменены дельтой, приняты.
## Находки
Нет находок. Дельта устраняет Medium-находку r1 полностью: каждое заявление К4/К4а/AC5/AC5а проверено чтением реального кода на цитируемых строках (см. «Как проверялось») и совпадает с тем, что там фактически есть — включая ранее непокрытую третью точку защиты и реальную (а не гипотетическую) последовательность вызовов `ws_config_set`.
## Что проверено и корректно (по дельте)
- **К4** технически верен: клиентская нормализация (`commitWallSegmentModel`) реально вызывается на всех пяти цитируемых местах и действительно предшествует отправке кандидата на сервер — это не гипотеза ревьюера, как в r1, а формулировка ТЗ, подтверждённая чтением.
- **К4а** технически верен: `_config_wall_segment_invariants` сегодня безусловно отвергает **любой** `room_drafts` при `model_version>=10`, независимо от содержимого; контракт корректно вводит её как третью, отдельную точку защиты и корректно ограничивает её сохранение только непустым случаем.
- **AC5** больше не утверждает недостижимую последовательность вызовов; обе его половины («сторож не бросает, когда drafts есть по обе стороны» и «схема принимает пустой / отвергает непустой ключ») проверяемы независимо друг от друга.
- **AC5а** технически обоснован: `commitWallSegmentModel` вызывает `migrateRoomDraftsToPartitions` для каждого пространства вне зависимости от `initialMigration`; после снятия безусловного `raise` (предмет К1/К2) результат гарантированно не будет содержать ключ `room_drafts` — именно это AC5а и обещает проверить.
- Пункт 2 «предположений» и «Затронутые файлы» теперь согласованы с фактическим кодом `ws_config_set`.
## Чего не проверял
- Мутанты AC7 (`scripts/mutation-gate.mjs`, `scripts/backend-test-guard.mjs`) — до появления кода мутант не существует, предмет код-ревью (не изменилось с r1).
- Не трассировал абсолютно все пути, ведущие к `_saveConfig`/`config/set`, для каждого типа структурной правки — ограничился пятью цитатами К4 и подтвердил, что каждая реально вызывает `commitWallSegmentModel`; исчерпывающий аудит всех писателей вне объёма ревью ТЗ (то же ограничение, что в r1).
- Не проверял, распространит ли будущая реализация правку корректно на оба зеркала и обе точки защиты одновременно — это предмет AC3/AC4/AC5/AC5а на этапе код-ревью, не этого раунда.
- Не запускал `tsc`/`npm test`/`npm run build`/pytest — неприменимо на этапе ТЗ: продуктового кода ещё нет.
## Вердикт
Зелёный. Единственная Medium-находка r1 закрыта по существу, а не декларативно: контракт теперь называет реальный механизм самолечения (клиентское зеркало `commitWallSegmentModel`) и реальную третью точку защиты (`_config_wall_segment_invariants`), AC5 не утверждает недостижимую на сервере последовательность вызовов, а добавленный AC5а даёт свидетеля клиентскому пути лечения. Расхождений между текстом ТЗ и кодом, найденных чтением, нет. ТЗ переходит в «Готово к разработке».
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `2977177283f9` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `c8ab6dc1d585a2d69709cd8e99ce0a6d9432b2a9`
```
git log --all --format='%H %T' | grep c8ab6dc1d585
```
- Тело issue: `817b8acec5b1a72336cf4cb0ecc1c5d583d5a9d5b6fa55d2aea6be5e50190eb1`
- Вердикт конвейера: `green` · High 0