# 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а даёт свидетеля клиентскому пути лечения. Расхождений между текстом ТЗ и кодом, найденных чтением, нет. ТЗ переходит в «Готово к разработке». --- ## Материал раунда - Ветка: `dev`, коммит `2977177283f9` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `c8ab6dc1d585a2d69709cd8e99ce0a6d9432b2a9` ``` git log --all --format='%H %T' | grep c8ab6dc1d585 ``` - Тело issue: `817b8acec5b1a72336cf4cb0ecc1c5d583d5a9d5b6fa55d2aea6be5e50190eb1` - Вердикт конвейера: `green` · High 0