# SPEC-REVIEW-314-r1 Issue: [#314](https://github.com/Matysh/houseplan-card/issues/314) — Model v8: рисование комнат отклоняется, draft IDs теряются, возможны ложные перегородки. Этап: spec (PROCESS.md §2.4). Заход: r1. Блокирующих циклов израсходовано: 0/4. ТЗ: `docs/specs/314-v8-draft-write-regression.md`, ветка `issue/314-v8-draft-write-regression`, SHA материала ревью `772b43b6` (коммит «docs: specify atomic v8 draft writes»). Вердикт: **зелёный**. ## Скоуп ревью Первый заход — полный разбор ТЗ, без раздела «Унаследовано» (§2.9/§2.10 PROCESS.md не применяются, r1). Проверялось: 1. соответствие обязательным разделам §7.1 (сценарий, что видит пользователь, проблема/причины, скоуп/не-скоуп, контракт поведения, UX, модель данных и миграция, i18n, AC1…ACn с доказательством, план автотестов, риски, откат, release-артефакты); 2. фактическая точность технических утверждений — построчная сверка каждой заявленной причины дефекта с текущим кодом на `HEAD`, а не с пересказом автора; 3. однозначность и проверяемость каждого AC, наличие способа доказательства; 4. отсутствие догадок, выданных за факт, вне блока «принято предположительно»; 5. согласованность с `docs/SCOPE.md` (J6 — «keep the plan true as the home evolves»), `docs/CONFIG-COMPATIBILITY.md` (текущий канон model v8) и `docs/USER-GUIDE.ru.md`/`src/i18n/*.json` (текст тостов). ## Как проверялось Полное чтение ТЗ (346 строк) и тела issue #314 с комментариями аналитики. Точечное чтение кода на упомянутых и смежных участках: - `src/houseplan-card.ts` — `_dropLegacySegments()` (сборка `room_drafts[].segments` как `{ cm }`, потеря `id`), `_writeConfig()` (место вызова перед `houseplan/config/set`), `_undoPoint()` (полная пересборка `segments` без id при откате точки), `_saveConfigDebounced()` (catch-ветки `wall_model_client_outdated`/`conflict`/generic — только toast, без rollback, кроме `conflict`), `_commitPhysicalGeometry()` и `_pendingPhysicalWrites` (подтверждено: `before` уже хранится как «earliest», `pending?.before || historyBefore`), `_finishWallChain()` (материализация активного draft в `partitions`). - `custom_components/houseplan/validation.py` — `_legacy_wall_model_projection`, `_legacy_wall_geometry_projection`, `_wall_catalog_projection`, `_restore_wall_model_fields`, `validate_wall_model_transition()`, точное место и текст `"v8 draft wall segments require ids"` (L1827-1828). - `docs/CONFIG-COMPATIBILITY.md` («Stable wall identity — model v8») — сверка, что текущий канон действительно описывает грубую (не разделённую) проверку, которую ТЗ предлагает уточнить, а не переписать заново. - `src/i18n/ru.json` / `en.json` — точный текст обоих тостов, упомянутых в issue, совпадает дословно; ТЗ не предлагает менять и не изобретает формулировки (§9: «Новых i18n-ключей нет»). - Существование и актуальность путей, названных в ТЗ: `tests_backend/test_wall_segment_model.py`, `docs/TESTING.md` (уже содержит раздел «Stable wall-segment identity (#282)»), `scripts/check-docs.mjs --external`, `npm run invariants`, `npm run golden:verify` — все существуют и соответствуют описанию. Не проверялось (не нужно на этапе spec): запуск гейтов/тестов — кода нет, проверять нечего; ревью spec-этапа не прогоняет typecheck/test/build. ## Находки Ни одной High или Medium-находки, блокирующей выход в «Готово к разработке», не обнаружено. ### Low-1 (снята с записью). Сценарий не называет персону/job явно `docs/specs/314-v8-draft-write-regression.md` §1 описывает сценарий («Пользователь рисует комнату в редакторе Плана») без прямой ссылки на `docs/SCOPE.md` (персона Home admin, job J6 — «Keep the plan true as the home evolves»). PROCESS.md §7.1 просит указывать персону и поверхность явно. **Почему не блокирует:** редактор Плана — по определению `docs/SCOPE.md` инструмент только для Home admin, второй персоны там физически нет (View/kiosk не пишут `room_drafts`). Двусмысленности, кто встретит этот дефект и на какой поверхности, не возникает; аналитический комментарий владельца уже явно называет J6. Снимаю без правки ТЗ. ### Low-2 (снята с записью). Нет отдельного раздела «план автотестов» Список тестов есть, но распределён между §9 «Затронутые модули», блоком «Обязательные регрессионные тесты» в теле issue и полем «Доказательство» каждого AC1…AC10, а не собран под одним заголовком «план автотестов», формально ожидаемым PROCESS.md §7.1. **Почему не блокирует:** содержательно план полон и избыточно точен — для каждого нормативного пункта контракта (§4, §5, §6) есть привязанный AC с именованным способом доказательства (`frontend unit`, `backend schema fixture`, `parametrized backend tests`, `browser/fake-WS smoke`, `model-invariant unit + review кода`), а AC5 отдельно требует «тест обязан падать на `dev` до исправления». Организационный, не содержательный пробел. ## Что проверено и корректно - **Root cause A (потеря ID при sanitation/undo) — подтверждён построчно.** `_dropLegacySegments()` (`src/houseplan-card.ts`, ветка `room_drafts`) пересобирает каждый `segment` как `{ cm }`; `id` действительно не копируется. `_writeConfig()` действительно вызывает эту функцию непосредственно перед `houseplan/config/set`. `_undoPoint()` действительно пересобирает **весь** массив `segments` из `_draftSegmentCms` (не только последний элемент) — при откате одной точки теряют id все N-1 уцелевших сегментов, не только удалённый. ТЗ описывает это точно (§2.1) и корректно ставит AC2 требованием сохранения id именно для «уцелевших N-1», а не только «незатронутых». - **Backend-ошибка воспроизводится детерминированно.** `validation.py` L1826-1828: `if any(not segment.get("id") for segment in draft.get("segments", [])): raise vol.Invalid("v8 draft wall segments require ids")` — точный текст сообщения из симптома пользователя совпадает. - **Root cause B (ложный «устаревший клиент») — подтверждён.** `_legacy_wall_geometry_projection()` включает `rooms`, `walls`, `open_spans`, **весь** `room_drafts` (без id, только `points`+`cm`) и **весь** `partitions` as-is, плюс `openings` (id/type/x/y/angle/length, без host). `validate_wall_model_transition()` при `old_model>=8, new_model>=8` сравнивает эту проекцию целиком и при расхождении, но неизменном `wall_segments`, кидает `wall_model_client_outdated` — независимо от того, что именно изменилось. Значит правка одного draft-сегмента, добавление partition или перемещение opening действительно триггерят ложный отказ уже сегодня, если контурный каталог не менялся. Предложенное разделение на catalog-coupled (§5.1: rooms id/poly/open_to, walls, open_spans) и self-identifying (§5.2: room_drafts, partitions, wall_columns, openings) проекции корректно ложится на реальный код: `wall_columns` в текущей legacy-проекции вообще отсутствует, то есть уже не участвует в false-positive — включение его в контракт лишь фиксирует существующее верное поведение регрессионным тестом (AC3.4), не притворяется, что чинит несуществующий баг. - **Механизм отката rejected-геометрии (§6) не изобретает новую инфраструктуру, а достраивает существующую.** `_pendingPhysicalWrites` действительно уже хранит «earliest» `before` (`pending?.before || historyBefore` в `_commitPhysicalGeometry()`) — §6.1 корректно описывает уже существующий факт, а не новое требование. Не хватает только проводки catch-ветвей `wall_model_client_outdated` и generic-ошибки до этого состояния (сегодня это делает только `conflict`) — именно так и сформулирован AC6/AC7. - **Разбор продуктового риска «более новая команда теряется вместе с отклонённой».** §6.3 и AC7 явно фиксируют: при отказе F1 откатываются **обе** команды (F1 и более новая F2), а не только отклонённая. Это потенциально видимая пользователю потеря работы, поэтому я целенаправленно проверял, не должен ли это быть продуктовый вопрос владельцу, а не «техническое предположение» (§16 п.3). Вывод: не должен — (а) это лишь расширяет уже существующий прецедент в коде (`conflict`-ветка уже делает `_cancelPath()` + полный `_reloadConfigOnly()`, то есть уже отбрасывает локальный путь целиком), (б) после исправления root cause B легитимное последовательное рисование стен вообще перестаёт получать `wall_model_client_outdated`, так что путь отката — не основной сценарий, а страховка от настоящих отказов (схема, сеть, реальный stale-writer), (в) поведение явно протестировано отдельным AC7, а не спрятано. Решение корректно оставлено «принято предположительно» технической категорией. - **Ни одна причина не выдана за факт без доказательства там, где доказательства нет.** §2.3/§3 явно помечают связь «отклонённый draft → лишняя partition» как «правдоподобный механизм», требующий отдельного доказывающего теста (это и есть AC6/failure smoke) — не заявлено как решённое. - **Текст ошибок пользователю не меняется и не изобретается.** Оба toast-текста, названные в issue, сверены дословно с `src/i18n/ru.json`/`en.json`; ТЗ явно не добавляет новых i18n-ключей — верно. - **Совместимость с каноном.** `docs/CONFIG-COMPATIBILITY.md` («Stable wall identity — model v8») сегодня описывает именно ту грубую проверку, которую ТЗ сужает; ТЗ включает обновление этого файла в затронутые модули (§9) — значит канон не разойдётся с кодом после реализации. - **Обязательные разделы §7.1 присутствуют по существу**: сценарий и видимый результат (§1), проблема/причины (§2), скоуп/не-скоуп (§7/§8), контракт поведения (§4-§6, весьма подробно), модель данных/миграция (§4 — «Persisted schema и model_version не меняются»), i18n (§9), AC1…AC10 с доказательством (§10), риски (§13, таблица риск/мера), откат (§14), release-артефакты (§15). Отсутствие отдельных заголовков «проблема» и «UX» — организационный вопрос, содержание есть (см. Low-2 по аналогии); UX явно закрыт фразой «Новых кнопок, настроек и режимов нет» (§1) и §11. - **Не-скоуп корректно исключает соседние риски.** #306 (заблокирован), автоматическая чистка существующих неоднозначных `partitions`, смена модели толщины/визуала стен — всё явно выведено за рамки и не создаёт скрытого расширения скоупа под видом фикса. - **Assumptions-блок (§16)** используется по назначению: все четыре пункта — действительно технические решения (граница open_spans/#306, критерий безопасности opening-only правки, стратегия «терять весь pending batch», «не удалять существующий мусор») с пометкой «поменять свободно», не продуктовые вопросы, требующие ответа владельца. ## Чего не проверял - Не запускал гейты (`typecheck`/`test`/`build`) — на этапе spec-review кода нет, проверять нечего, это будет делом code-review. - Не проверял `_draftSegmentsForPath()` и полный код `commitWallSegmentModel()` построчно — не требовалось для оценки ТЗ: контракт §4 описывает целевое поведение этой функции, а не текущую реализацию, и её корректность — предмет code-review, а не spec-review. - Не читал файлы владельца (`houseplan-space-convergence-test-...json`, `2026-08-26_07-49-10.png`) — не приложены к issue/репозиторию для ревьюера и явно помечены «публично не прикладывать»; ТЗ уже хеджирует любые выводы из них как «не позволяет надёжно определить» / «требует отдельного теста», так что для оценки ТЗ они не нужны. ## Итог ТЗ технически точно (все процитированные причины сверены построчно с кодом на HEAD и подтверждены), полно по требованиям §7.1, содержит десять однозначных, проверяемых AC с указанным способом доказательства, корректно разделяет продуктовые предположения (нет ни одного) от технических (явно помечены в §16), и не выдаёт недоказанные гипотезы за факт. Обе найденные Low-находки — организационные, не меняют содержание ТЗ и сняты без правки. Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0