Files
houseplan-card/docs/reviews/CODE-REVIEW-340-r1.md
T
2026-08-28 11:34:13 +00:00

14 KiB
Raw Blame History

CODE-REVIEW-340-r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/340
  • Этап: код-ревью (PROCESS.md §2.7)
  • Заход: r1 · блокирующих циклов израсходовано 0 из 4
  • SHA материала ревью: 3e437ca3ec760cad21ca3e15048487bca6ea16b7 (git rev-parse HEAD перед подведением итогов)
  • Диапазон: git diff origin/dev...HEAD (base 356b187e, 2 продуктовых/гейт-коммита: c9981e68, 3e437ca3; 9239f9d3 — коммит публикации SPEC-REVIEW-340-r1, не код)
  • Ветка приведена к dev конвейером до ревью (1 коммит dev лёг поверх, 5f4d944f → 3e437ca3). Разбор ниже — полный, не по дельте: это первый код-ревью цикл этого issue, и после ребейза материал — другой код (§7.2).

Скоуп задачи

Аудит C4: ws_config_set принимал запись без expected_rev поверх непустого store, ограничиваясь warning-ом — тихая потеря чужой работы при двух клиентах. J6 (docs/SCOPE.md) прямо включает multi-client live sync и optimistic locking, спецификация уже прошла зелёное ревью (docs/reviews/SPEC-REVIEW-340-r1.md, eb8a5941). ТЗ фиксирует полный трек (публичный WS-контракт, риск >3) и 7 AC. Код-ревью проверяет: доказан ли каждый AC, не сломан ли позитивный путь и задокументирован ли новый контракт там, где раньше был указан warning-only.

Как проверялось

Гейт Команда Результат
Typecheck npx tsc --noEmit OK, без ошибок
Frontend unit npm test 1462 pass, 1 skip, 0 fail
Build + bundle sync npm run build OK; dist/** в диффе отсутствует — src/***.ts вообще не тронут этим issue, сверка трёх копий неприменима
Docs fingerprint node scripts/check-docs.mjs Documentation checks passed (7 files, 10 external links) — запущен по правилу "трогает src/**"? Нет (src не тронут), прогнан всё равно, т.к. USER-GUIDE.md/.ru.md в диффе — дёшево и подтверждает целостность якорей
Backend HA harness python3 -m pytest tests_backend -q (пакеты pytest-homeassistant-custom-component и др. установлены вручную в среде ревью — их не было) 437 passed, 1 skipped, 1 error
Backend targeted python3 -m pytest tests_backend/test_ha_websocket.py -k "config" -v все 13 отобранных PASSED (включая test_config_rev_conflict, test_issue_340_config_set_without_revision_is_bootstrap_only)
Frontend inventory guard node --test test/coordinate-write-barrier-guard.test.mjs 2/2 pass
Smoke-select node scripts/smoke-select.mjs --base origin/dev --head HEAD «Исполняемого frontend-диффа нет (src/**/*.ts не тронут). Browser-smoke этим диффом не выбираются — выбирать нечего.» → 0 браузерных смоков запущено, обоснованно
single-source-numbers node --test test/single-source-numbers.test.mjs 3/3 pass (диффом не затронуто, прогнан для очистки совести — дёшево)
Тест умеет падать вручную: применил новый tests_backend/test_ha_websocket.py к origin/dev (код без фикса) test_issue_340_config_set_without_revision_is_bootstrap_only → FAILED — assert not True (второй клиент без rev получал success). На HEAD тот же тест зелёный → тест реально доказывает контракт, не тавтология

1 ошибка (error) в полном backend-прогоне — не регрессия. Teardown-ассерт HA test harness про «протёкший» поток _run_safe_shutdown_loop (AssertionError: assert (False or False), threading._DummyThread). Воспроизвёл тот же error на чистом origin/dev (test_config_set_purges_tombstoned_and_absent_trails_durably, без единой строки из этого issue) — окружение/harness-флейк, не связан с диффом. Сам тест перед teardown — PASSED.

Не проверялось, и почему

  • npm run golden:verify — не нужен: src/** не тронут ни строкой, визуал не меняется (спецификация §15 и сам diff это подтверждают).
  • Инварианты модели (npm run invariants) — не нужны: диффом не задета геометрия, layout, marker.space, open_spans или толщина стен; изменение — исключительно ревизионный guard в config/set и его документация.
  • Полный набор demo/smoke_*.mjs — не нужен и не выбран инструментом (см. таблицу выше): фронтенд не менялся вообще.
  • Performance-профили — не названы в AC, путь не производительно чувствительный (одна дешёвая integer-проверка на уже открытом write_lock).

AC — построчно

AC Что требуется Проверено Как
AC1 Первая запись без rev на пустом store — ok; любая следующая без rev — conflict ✅ код websocket_api.py:1293-1318 (guard внутри write_lock, до валидации); backend-тест test_issue_340_config_set_without_revision_is_bootstrap_only — bootstrap даёт rev=1, повторная запись без rev даёт conflict
AC2 Missing-rev conflict не меняет config/rev/backup/event, включая semantic no-op ✅ код: return на строке 1312 происходит до msg["config"] == data.get("config") (no-op-детекция, 1374), до async_save_config_state, до _discard_optimizer_snapshot, до hass.bus.async_fire; тест явно шлёт байт-в-байт совпадающий first_config без rev вторым клиентом и проверяет stored_before равенство, OPTIMIZE_BACKUP неизменность, config_events == []
AC3 Два конкурентных клиента: после первого коммита второй blind write отклонён ✅ тест test_issue_340_config_set_without_revision_is_bootstrap_only: first_client коммитит rev=1, stale_client (не видевший этот коммит) получает conflict; сериализация гарантирована тем же write_lock, которым защищены все остальные writer'ы (не новый механизм)
AC4 Explicit верный rev — success/no-op; explicit устаревший rev — conflict ✅ не задето изменением (строки 1313-1318 без правок) + существующий test_config_rev_conflict зелёный + новый тест дополнительно проверяет retry с expected_rev=1 → rev=2
AC5 Ровно один config/set во frontend, всегда с expected_rev ✅ scripts/coordinate-write-barrier-guard.mjs:39 требует expected_rev: this\._cfgRev в том же окне, что canonicalCandidate; production src/houseplan-card.ts:6887-6890 содержит ровно это; checkCoordinateWriteBarriers() вручную прогнан → []; новый unit-тест мутирует fixture (убирает expected_rev) и подтверждает, что guard это ловит — прогнан, зелёный
AC6 Публичная документация не обещает warning-only, описывает bootstrap-only compatibility ✅ docs/ARCHITECTURE.md (таблица + новый абзац), docs/CONFIG-COMPATIBILITY.md (новый раздел «Revision-less config writers (#340)»), docs/TESTING.md (пункт B2-B5 переписан) — все три больше не говорят про warning-only; node scripts/check-docs.mjs зелёный
AC7 UI/i18n/schema/model/export version/View/kiosk/touch и позитивный write-path не меняются ✅ git diff --stat не содержит ни одного файла src/**, custom_components/**/translations/**, manifest.json; позитивный путь (expected_rev совпадает) — тот же код, что и раньше (строки 1313 и далее не менялись); npx tsc --noEmit/npm run build зелёные

Все 7 AC доказаны автотестом; там, где тест — дисциплина «тест умеет падать» подтверждена явным прогоном на pre-fix коде (см. таблицу гейтов). AC6/AC7 — частично «проверено чтением, не исполнением» в части текста документации и отсутствия diff в защищённых путях, что и требуется для этого типа критерия.

Находки

Нет ни одной находки уровня High или Medium. Одно наблюдение уровня Low, не требующее правки:

  • docs/ARCHITECTURE.md в строке таблицы houseplan/config/set убрал ? у expected_rev (было expected_rev?), в то время как houseplan/layout/set на строке выше сохраняет expected_rev?. На первый взгляд несогласованность, но это осознанная и корректная асимметрия: layout/set действительно остаётся warning-only (тот самый дефект, вынесенный ревьюером ТЗ в отдельный #356 как вне скоупа #340), а config/set через 3 строки ниже таблицы получает отдельный абзац, ровно объясняющий, что поле формально vol.Optional, но семантически обязательно везде, кроме bootstrap. Снято без правки — документ точен, объяснение рядом.

Что проверено и корректно

  • Guard стоит внутри rt.write_lock, сразу после async_load()/current_rev, до CONFIG_SCHEMA/validate_* и до сравнения no-op — соответствует §6.2 ТЗ построчно.
  • Warning-лог после правки не содержит конфига/имён маркеров: тест явно проверяет "stale-secret" not in caplog.text.
  • Bootstrap-исключение безопасно и при гонке двух bootstrap-клиентов: оба идут через тот же write_lock, второй уже видит rev=1 и попадает в ветку conflict — не новый примитив, тот же, что уже страхует остальные writer'ы этого файла.
  • Ошибка пользователю (toast.conflict) не меняется и не начинает показывать число ревизии карточке: src/houseplan-card.ts:7068-7070 берёт только e.code === 'conflict' и показывает статичный переведённый текст, e.message (с текстом ревизии для прямого WS-клиента) не парсится и не отображается — риска «одно число, два источника» это изменение не создаёт.
  • Оба changelog (docs/CHANGELOG.md, docs/CHANGELOG.ru.md) отредактированы в том же коммите 3e437ca3, что несёт User-Visible: yes — трейлер-требование соблюдено. docs: specify config revision enforcement (c9981e68, ТЗ+specs/README) отдельно несёт User-Visible: no — верно, это не продуктовое изменение.
  • Никаких файлов класса D (dist/**, custom_components/houseplan/frontend/**) в диффе нет — ожидаемо, поскольку src/** не менялся вовсе.
  • Medium-находка спец-ревью вне скоупа (layout/set, тот же класс C4) уже заведена спек-ревьюером как #356 — не задача этого код-ревью её дублировать или чинить.

Вывод

Зелёный. Все 7 AC доказаны и воспроизведены самостоятельно (включая подтверждение, что новый backend-тест падает на pre-fix коде). Дешёвые и уместные по диффу гейты — все зелёные. Находок, требующих правки, нет.