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

16 KiB
Raw Blame History

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