Files
houseplan-card/docs/reviews/SPEC-REVIEW-340-r1.md
T
2026-08-28 11:26:19 +00:00

17 KiB
Raw Blame History

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 — не блокирует переход #340 в «Готово к разработке». Low: 1, снят решением ревьюера с записью выше (пункт «docs/ARCHITECTURE.md»), без правки текста ТЗ.