mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
200 lines
17 KiB
Markdown
200 lines
17 KiB
Markdown
# 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`»), без правки текста ТЗ.
|