17 KiB
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), а не на слово автора:
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).src/houseplan-card.ts:6890— единственная точка записи frontend (_sendConfigCandidate) действительно всегда шлётexpected_rev: this._cfgRev;scripts/coordinate-write-barrier-guard.mjsуже требует ровно одинconfig/setв проде — подтверждает заявление ТЗ «сегодня фронтенд не получает нового пути».tests_backend/test_ha_websocket.py:438-453(test_config_rev_conflict) — существующий тест покрывает только явный устаревшийexpected_rev, не отсутствие поля. Значит AC1-AC3 (пропущенная ревизия) действительно новый, непокрытый тестами сценарий, а не то, что уже доказано — ТЗ не выдаёт старое за новое.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 (два конкурентных клиента) технически реализуем по уже принятому в проекте паттерну, а не является гипотезой.docs/TESTING.md:1066-1067— дословно подтверждает claim «документация всё ещё описывает warning-only путь» («a config/set without expected_rev over a non-empty store logs a warning»).docs/ARCHITECTURE.md:860-868— WS API таблица действительно помечаетexpected_revкакexpected_rev?без прозы про warning-only fallback (см. находку Low ниже).docs/CHANGELOG.md:4665(v1.4.4 — CRITICAL fix: configuration race) — подтверждает claim «параметр существует с v1.4.4».src/i18n/{en,ru,de}.json:305(toast.conflict) — подтверждено: ключ уже существует на всех трёх локалях с нужным смыслом, новый ключ действительно не требуется.docs/USER-GUIDE.ru.md:1652-1658(раздел «Несколько клиентов») — уже существует и не содержит утверждений, которые бы противоречили или дублировали предлагаемую правку; расширение раздела по AC6 корректно нацелено.test/coordinate-write-barrier-guard.test.mjs— сегодня без fixture-mutation негативного теста; в разделе «план автотестов» ТЗ (§11.2) корректно описывает это как будущую работу реализации, а не как уже существующий факт (проверено на формулировке — раздел «план», а не «подтверждено»). Прецедент подобных негативных фикстур в проекте есть (test/coincident-partitions.test.mjs,test/fixture-wall-key.test.mjsи др.) — AC5 реализуем.- Пройден по PROCESS.md §7.1 обязательный список разделов ТЗ — все присутствуют (сценарий, что увидит человек, проблема, скоуп/не-скоуп, контракт, UX/i18n, модель данных/миграция, AC1…AC7 с доказательством, план автотестов, риски, откат, release-артефакты).
- Проверено соседнее поведение:
ws_layout_set(custom_components/houseplan/websocket_api.py:567-600) — та же схема guard-а. См. находку Medium ниже. - Прочитаны связанные 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; guardif "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»), без правки текста ТЗ.