# SPEC-REVIEW-340-r1 - Issue: https://github.com/Matysh/houseplan-card/issues/340 - Этап: ТЗ (S4-spec-review), заход r1, полный трек - Артефакт ТЗ: `docs/specs/340-config-set-revision.md` - Ветка: `issue/340-config-set-revision`, SHA на момент ревью: `eb8a594139d283cd47550423a5c0729ac6793979` - Диапазон материала: `git diff origin/dev..origin/issue/340-config-set-revision` (изменяет только `docs/specs/340-config-set-revision.md` и одну строку `docs/specs/README.md` — продуктовый код не тронут, что соответствует стадии) - Вердикт: **зелёный** ## Скоуп Проблема: `ws_config_set` принимает `expected_rev` как опциональное поле и при его отсутствии поверх уже ненулевой ревизии store лишь пишет warning и продолжает полную перезапись — второй клиент (вкладка/устройство/сторонний WS) может молча стереть чужую работу. ТЗ вводит fail-closed guard: bootstrap (`rev=0`, без поля) остаётся разрешён, а любая повторная запись без `expected_rev` поверх `rev > 0` получает существующий код `conflict`, без изменения store/rev/backup/event. Явный устаревший `expected_rev` продолжает получать тот же `conflict` (уже работает сегодня). Трек — полный (соответствует критериям §5 PROCESS.md: риск > 3, меняется публичный WS/compatibility-контракт), это подтверждено аналитикой владельца в комментарии issue. Персона и сценарий (`docs/SCOPE.md`, J6 «keep the plan true as the home evolves» — multi-client live sync, optimistic locking): администратор дома редактирует общий план с двух вкладок/устройств; сейчас устаревшая копия может затереть новую без предупреждения. Это прямое попадание в J6, не догадка. ## Как проверялось Ревью ТЗ на стадии spec: продуктовый код не менялся, поэтому дешёвые гейты (`typecheck`/`test`/`build`) неприменимы к этому диффу — диф состоит из документации класса C. Основной метод — проверка каждого утверждения раздела 3 «Подтверждено на origin/dev» и каждого AC против фактического кода/тестов/доков на `dev` (`eb8a5941`), а не на слово автора: 1. `custom_components/houseplan/websocket_api.py:1291-1300` — прочитан код `ws_config_set`: подтверждено дословно то, что описывает ТЗ (`"expected_rev" not in msg and current_rev` → только `_LOGGER.warning`, без `return`; проверка ревизии — внутри `async with rt.write_lock`, до CPU-валидации и `async_save_config_state`). 2. `src/houseplan-card.ts:6890` — единственная точка записи frontend (`_sendConfigCandidate`) действительно всегда шлёт `expected_rev: this._cfgRev`; `scripts/coordinate-write-barrier-guard.mjs` уже требует ровно один `config/set` в проде — подтверждает заявление ТЗ «сегодня фронтенд не получает нового пути». 3. `tests_backend/test_ha_websocket.py:438-453` (`test_config_rev_conflict`) — существующий тест покрывает только явный устаревший `expected_rev`, не отсутствие поля. Значит AC1-AC3 (пропущенная ревизия) действительно новый, непокрытый тестами сценарий, а не то, что уже доказано — ТЗ не выдаёт старое за новое. 4. `tests_backend/test_ha_websocket.py:1229-1300` — найден прецедент теста с двумя `hass_ws_client(hass)` в одном тесте (`test_late_commit_of_one_client_never_deletes_another_client_s_plan`), подтверждающий, что AC3 (два конкурентных клиента) технически реализуем по уже принятому в проекте паттерну, а не является гипотезой. 5. `docs/TESTING.md:1066-1067` — дословно подтверждает claim «документация всё ещё описывает warning-only путь» («a config/set without expected_rev over a non-empty store logs a warning»). 6. `docs/ARCHITECTURE.md:860-868` — WS API таблица действительно помечает `expected_rev` как `expected_rev?` без прозы про warning-only fallback (см. находку Low ниже). 7. `docs/CHANGELOG.md:4665` (`v1.4.4 — CRITICAL fix: configuration race`) — подтверждает claim «параметр существует с v1.4.4». 8. `src/i18n/{en,ru,de}.json:305` (`toast.conflict`) — подтверждено: ключ уже существует на всех трёх локалях с нужным смыслом, новый ключ действительно не требуется. 9. `docs/USER-GUIDE.ru.md:1652-1658` (раздел «Несколько клиентов») — уже существует и не содержит утверждений, которые бы противоречили или дублировали предлагаемую правку; расширение раздела по AC6 корректно нацелено. 10. `test/coordinate-write-barrier-guard.test.mjs` — сегодня без fixture-mutation негативного теста; в разделе «план автотестов» ТЗ (§11.2) корректно описывает это как будущую работу реализации, а не как уже существующий факт (проверено на формулировке — раздел «план», а не «подтверждено»). Прецедент подобных негативных фикстур в проекте есть (`test/coincident-partitions.test.mjs`, `test/fixture-wall-key.test.mjs` и др.) — AC5 реализуем. 11. Пройден по PROCESS.md §7.1 обязательный список разделов ТЗ — все присутствуют (сценарий, что увидит человек, проблема, скоуп/не-скоуп, контракт, UX/i18n, модель данных/миграция, AC1…AC7 с доказательством, план автотестов, риски, откат, release-артефакты). 12. Проверено соседнее поведение: `ws_layout_set` (`custom_components/houseplan/websocket_api.py:567-600`) — та же схема guard-а. См. находку Medium ниже. 13. Прочитаны связанные issue #220 и #224 (упомянуты в ТЗ как related) — ссылки корректны и не искажают их содержание. ## Находки ### Medium (вне скоупа) — заведён отдельный issue **`ws_layout_set` несёт тот же класс дефекта C4, что и `ws_config_set`, и сейчас находится вне скоупа #340.** - Файл: `custom_components/houseplan/websocket_api.py:585` - Воспроизведение: `expected_rev` — `vol.Optional`; guard `if "expected_rev" in msg and msg["expected_rev"] != current_rev` не срабатывает вовсе, если поле отсутствует, — код проваливается к `async_save_layout_state` независимо от `current_rev`. В отличие от `ws_config_set`, здесь нет даже диагностического `_LOGGER.warning` на этот путь — это тише, чем то, что чинит #340. - Сценарий отказа: второй клиент без прочитанной ревизии layout (например, очень старая карточка или сторонний WS-клиент) отправляет `houseplan/layout/set` без `expected_rev` поверх непустого layout store — позиции устройств, выставленные другим клиентом, теряются молча, без какого-либо предупреждения в лог или клиенту. - Не покрыто тестами: в `tests_backend/test_ha_websocket.py` нет теста «`layout/set` без `expected_rev` поверх `rev > 0`»; `docs/TESTING.md:1066-1067` утверждает только «layout/set honours expected_rev», что верно лишь пока поле передано, и не описывает эту дыру. - Почему не в этой задаче: ТЗ #340 явно ограничивает скоуп конфигом (аудит C4 называл только `ws_config_set:1307-1315`), `layout/set` — в разделе «Не-цели» ТЗ. Это корректная граница автора, а не пропуск — чужой скоуп не правится из этой ветки (PROCESS.md §2.4, §12). - Действие: заведён https://github.com/Matysh/houseplan-card/issues/356 (`bug`, `P3`, `S1-new`) со ссылкой на #340 и предложенным AC, симметричным матрице #340 §6.1. ### Low — не блокирует, отмечено с решением «оставить как есть» **Раздел 3 ТЗ переоценивает точность цитаты `docs/ARCHITECTURE.md`.** - Файл: `docs/specs/340-config-set-revision.md`, раздел 3 («Подтверждено на origin/dev») - Формулировка ТЗ: «`docs/ARCHITECTURE.md` и `docs/TESTING.md` всё ещё описывают отсутствие ревизии как разрешённый warning-only путь». - Факт: для `docs/TESTING.md` цитата дословно верна (строка 1066-1067). Для `docs/ARCHITECTURE.md` это неточно: строка 866 лишь помечает поле как `expected_rev?` (опционально) в таблице WS API и не содержит прозы про warning-only fallback — то есть документ не «описывает» это поведение явно, а просто не запрещает его умолчанием. - Решение ревьюера: **низкая серьёзность, вправить не требуется отдельно** — `docs/ARCHITECTURE.md` и так значится в списке файлов на обновление (§10 ТЗ, «WS API и optimistic-locking contract»), и после реализации таблица всё равно перестанет молчаливо подразумевать permissive-поведение. Неточность не меняет ни один AC и не вводит читателя ТЗ в заблуждение относительно объёма работы. ## Что проверено и корректно - Контракт §6.1/6.2 (матрица + порядок guard-ов) соответствует фактическому коду: место вставки проверки (внутри `write_lock`, до CPU-валидации, до `msg["config"] == data.get("config")` no-op шортката) описано точно и закрывает риск «no-op как обход CAS» (§6.2 п.6) — реальный риск, а не надуманный: сегодняшний код проверяет no-op раньше guard-а не будет, если реализация последует порядку из ТЗ. - Bootstrap-исключение при двух одновременных клиентах рассуждено верно: сериализация через `write_lock` гарантирует, что второй bootstrap увидит `current_rev=1` и попадёт в fail-closed ветку (не гонка). - Оставление `expected_rev` как `vol.Optional` в voluptuous-схеме — осознанное и верно обоснованное решение (иначе отсутствие поля возвращало бы общий `invalid_format` вместо доменного `conflict`). - Явно оставлен `layout/set`, `import/optimize` вне скоупа — корректная граница, а не увиливание (см. Medium-находку — она подтверждает, что границу провели по факту аудита C4, а не для удобства). - Ни одной догадки не выдано за факт: раздел 16 «Принятые предположительно технические решения» честно маркирует то, что не требует продуктового ответа владельца, и все пункты там действительно технические (код ошибки, optional-схема, safe legacy window, повторное использование существующего inventory-скрипта). - Продуктовых открытых вопросов нет, и это оправдано: единственная содержательная развилка («что считать безопасным legacy-исключением») уже решена самим владельцем текстом в комментарии S2-analysis («любая запись без rev поверх сохранённого конфига должна fail-closed вернуть existing conflict»), и ТЗ просто реализует это дословно, не домысливая ничего сверх. - AC1-AC7 однозначны, каждый указывает способ доказательства (`backend HA websocket test`, `executable coordinate writer guard + unit negative fixture`, `npm run docs:accept` + ревью кода, `typecheck/build` + ревью кода) и формулировка допускает объективную проверку «прошёл/не прошёл». - i18n, UX, touch, миграция, откат, release-артефакты — присутствуют, содержательны, не расширяют скоуп сверх задачи. - DoR-совместимость (PROCESS.md §2.5): все обязательные пункты (AC с доказательством, затронутые файлы, i18n, compatibility, touch, откат, release-артефакты, отсутствие открытых продуктовых вопросов) в ТЗ присутствуют. ## Чего не проверял - Не гонял `typecheck`/`test`/`build`/`docs-check` — диапазон ревью не содержит ни одного файла класса A/B (только `docs/specs/**`), поэтому дешёвые гейты этого раунда неприменимы; они станут обязательны на код-ревью. - Не проверял `docs/USER-GUIDE.md` (английская версия) построчно — только русскую, как того требует инструкция ревью терминологии интерфейса; ТЗ ссылается на обе локали симметрично, и содержательных решений, специфичных для английской версии, ТЗ не описывает. - Не проверял глубоко пути `layout/update`, `import`, `plan/optimize` — они явно вне скоупа (§5 ТЗ), кроме поверхностной сверки, что `plan/optimize` использует «both expected revisions» (docs/ARCHITECTURE.md:867), что согласуется с тем, что этот путь не нуждается в такой же правке. - Не выполнял ручное/браузерное тестирование — на этапе spec-review это не предусмотрено процессом (нет продуктового кода для запуска). ## Вердикт Зелёный. High: 0. Medium: 1, вне скоупа задачи, заведён отдельным issue [#356](https://github.com/Matysh/houseplan-card/issues/356) — не блокирует переход #340 в «Готово к разработке». Low: 1, снят решением ревьюера с записью выше (пункт «`docs/ARCHITECTURE.md`»), без правки текста ТЗ.