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

200 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`»), без правки текста ТЗ.