16 KiB
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 внутри уже описанной подсистемы, без новой подсистемы и без ребейза — здесь просто нет кода и веток), поэтому объём разбора сокращается до дельты плюс всего, чего она касается:
- К4 переписан на реальный механизм самолечения;
- добавлен К4а — про третью точку защиты (
_config_wall_segment_invariants); - AC5 переформулирован — больше не утверждает несуществующую последовательность вызовов;
- добавлен AC5а — свидетель клиентского пути лечения;
- пункт 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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
c8ab6dc1d585a2d69709cd8e99ce0a6d9432b2a9git log --all --format='%H %T' | grep c8ab6dc1d585 - Тело issue:
817b8acec5b1a72336cf4cb0ecc1c5d583d5a9d5b6fa55d2aea6be5e50190eb1 - Вердикт конвейера:
green· High 0