mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,324 @@
|
||||
# CODE-REVIEW-462-r1
|
||||
|
||||
**Issue:** https://github.com/Matysh/houseplan-card/issues/462
|
||||
**Этап:** код-ревью (PROCESS.md §2.7), заход r1, блокирующих циклов израсходовано 0/4
|
||||
**Ревьюер:** Claude (роль «ревьюер кода», отдельная от роли «разработчик» — Codex)
|
||||
**Материал:** `git log --oneline origin/dev..HEAD` (16 коммитов, все с трейлером `Issue: #462`) и
|
||||
`git diff origin/dev...HEAD` на SHA `daccaf7ea54a01e13d4150cbd588893ad90a4672` — этот SHA совпадает
|
||||
с текущим `HEAD` в момент вывода вердикта (проверено `git rev-parse HEAD` непосредственно перед
|
||||
подведением итогов, §2.7 «вердикт привязан к SHA»).
|
||||
**ТЗ:** `docs/specs/462-card-resource-registration.md`, ревью ТЗ зелёное на r3
|
||||
(`docs/reviews/SPEC-REVIEW-462-r3.md`), AC1–AC13.
|
||||
|
||||
## 1. Скоуп диффа
|
||||
|
||||
74 файла, +4739/−450 (единственный `User-Visible: yes` коммит `fcf0ce80`, остальные 15 —
|
||||
`User-Visible: no`, тесты/документация/сгенерированные бандлы). Функционально:
|
||||
|
||||
- **Backend:** новый модуль `custom_components/houseplan/frontend_registration.py` (659 строк) —
|
||||
типизированный outcome регистрации Lovelace-ресурса, lifecycle retry на 1 секунду,
|
||||
одноразовое `persistent_notification`, честный System Health; `__init__.py` и `system_health.py`
|
||||
переведены на него; `manifest.json` получил `after_dependencies: ["lovelace"]`.
|
||||
- **Frontend:** новый чистый контроллер `src/version-recovery.ts` (331 строк, без DOM/Lit/HA) +
|
||||
адаптер `src/version-recovery-card.ts` (223 строки) + интеграция в `src/houseplan-card.ts`
|
||||
(89 строк диффа: баннер в `_renderRoot`, единая точка `_openMoreInfo`, `_syncVersionRecovery`
|
||||
на всех жизненных циклах).
|
||||
- **Документация:** README/USER-GUIDE EN+RU — развилка Storage/YAML `resource_mode`/legacy
|
||||
`mode: yaml`; статический parser в `scripts/check-docs.mjs`; оба changelog.
|
||||
- **Тесты:** 24 backend-теста (`tests_backend/test_ha_frontend_registration.py`), 3 новых
|
||||
frontend-unit-файла, `demo/smoke_version_recovery.mjs` (405 строк), 3 golden-сценария, 14 новых
|
||||
мутантов в `scripts/mutation-gate.mjs`.
|
||||
|
||||
Продуктовое поведение подтверждено проверкой на реальном исходнике HA (см. §5) — задача не
|
||||
меняет раздачу бандла/кэш-бастинг, только надёжность регистрации и явную диагностику.
|
||||
|
||||
## 2. Как проверялось — таблица гейтов
|
||||
|
||||
| Гейт | Прогнан | Результат |
|
||||
|---|---|---|
|
||||
| `npx tsc --noEmit`, `npm test`, `npm run build` + сверка 3 копий бандла | Нет, переиспользован | Validate зелёный на точном `HEAD` `daccaf7e`: https://github.com/Matysh/houseplan-card/actions/runs/33969641826 |
|
||||
| `node scripts/check-docs.mjs` (diff трогает `src/**`) | Нет напрямую, переиспользован | Часть того же зелёного Validate (job `docs`) |
|
||||
| `npm run golden:verify` (диффа рендера AC12) | Нет, переиспользован | Часть зелёного Validate (job `golden`); 3 новых baseline приняты коммитом `daccaf7e` с трейлерами `Release: v1.72.0-beta.5` + `Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33969189246` |
|
||||
| `python -m pytest tests_backend -q` | Нет, переиспользован | Часть зелёного Validate (job `backend`); локально pytest/homeassistant не установлены в этой ревью-сессии (см. §6) |
|
||||
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | **Да** | 98 «прямых совпадений» — разбор ниже (§4.3), большинство ложные |
|
||||
| `node demo/smoke_version_recovery.mjs` | **Да** (через мутанта, чистый прогон) | зелёный — `ok чистый прогон: node demo/smoke_version_recovery.mjs` |
|
||||
| `node scripts/mutation-gate.mjs --id=<14 мутантов #462>` | **Да**, 10 из 14 | 10/10 покраснели корректно (см. таблицу §5); 4 backend-мутанта не прогнаны локально (нет `pytest`/`homeassistant` в этой ревью-песочнице — дорогой гейт, разрешено читать вместо повторного прогона, §2.7) |
|
||||
| Чтение исходника HA (`homeassistant==2024.6.0`, минимально поддерживаемая версия) | **Да** | скачан `pip download homeassistant==2024.6.0`, подтверждено наличие `ConfigEntry.async_create_background_task(hass, target, name, eager_start=True)` — закрывает открытый вопрос по AC3 (см. §5) |
|
||||
| Инварианты модели (`npm run invariants`) | Не прогнан | diff не трогает геометрию/`layout`/`marker.space`/`open_spans` — неприменимо |
|
||||
| Performance-профили | Не прогнан | не названы в AC; единственное влияние на перф — бюджет бандла, уже пересчитан и задокументирован автором в `scripts/bundle-budget.mjs` (см. §7) |
|
||||
|
||||
## 3. Находки
|
||||
|
||||
**High: 0. Medium: 0 (в скоупе задачи и вне его). Low: 3, ни один не блокирует, оставлены с
|
||||
записью ниже.**
|
||||
|
||||
| # | Файл:строка | Что | Severity | Решение |
|
||||
|---|---|---|---|---|
|
||||
| L1 | `src/version-recovery-card.ts:176` | `viewOnly` включает `host._config?.kiosk === true`, хотя `VersionRecoveryController._reconcile()` (`src/version-recovery.ts:242`) уже сам не вызывает `isVersionReloadSafe()` вне kiosk-ветки. Задвоение безопасное (belt-and-suspenders), но при будущем рефакторинге кто-то может решить, что `viewOnly` — единственный источник kiosk-гейта, и убрать внешнюю проверку по ошибке | Low | Оставлено с записью; не влияет на корректность текущего кода |
|
||||
| L2 | `src/styles/base.styles.ts` (правила `.version-recovery*`, `:host(houseplan-card)`) | Стили лежат в общем `baseStyles`, который импортирует и `houseplan-space-card` (`src/space-card.ts:9,932`). Селектор `:host(houseplan-card)` корректно не матчится на space-card, поэтому визуальной утечки нет — это чистый вопрос лишних байт в бандле space-card | Low | Оставлено; `test/version-recovery-card-contract.test.mjs` уже фиксирует границу селектора |
|
||||
| L3 | `custom_components/houseplan/frontend_registration.py:537` | `entry.async_on_unload(state.cancel)` регистрируется при каждом вызове `async_setup_frontend_registration`, включая гипотетический повторный вызов без промежуточного unload — список on-unload callbacks мог бы расти. `cancel()` идемпотентен, и повторный вызов достижим только при нарушении контракта HA (setup без unload между вызовами), что не воспроизводится ни в одном тесте и не считается реальным путём в текущем HA lifecycle | Low | Оставлено с записью; не воспроизводится, не блокирует |
|
||||
|
||||
Ни один из этих трёх пунктов не меняет вывод AC и не варьирует поведение, наблюдаемое
|
||||
пользователем — вердикт по ним: **правится не будет, оставлены как документированные
|
||||
наблюдения** (решение ревьюера, §2.7 «Low либо правится, либо снимается с записью»).
|
||||
|
||||
Отдельно отмечаю (не находка, наблюдение по §2.4/2.6 «скоуп не расширяется»): коммит `65d38b24`
|
||||
(`fix: keep continuity cache read-only until load`, `Issue: #462`, `User-Visible: no`) добавляет
|
||||
guard `!this._loadOk` в `_seedHiddenDevices` и `_syncAreaRelocations`
|
||||
(`src/houseplan-card.ts:3958,5120-5121`) — защита от того, что continuity-cache-кадр,
|
||||
нарисованный до завершения авторитетной загрузки config+layout, мог бы вызвать деструктивную
|
||||
запись (перезаписать серверную фильтрацию или удалить валидный snapshot area-relocation) поверх
|
||||
устаревшего состояния. Это тот же инвариант `_loadOk`, от которого зависит новый предикат
|
||||
`initialFrameSettled` (`version-recovery-card.ts:155`), поэтому чинить его в рамках этой же задачи
|
||||
оправдано — это не отдельная попутная правка, а условие корректности самого AC8. Пользовательски
|
||||
видимый эффект (защита от редкой потери непримечательного состояния при холодном старте) реален,
|
||||
но обнаружен как превентивное упрочнение, а не как воспроизведённый дефект; `User-Visible: no`
|
||||
для него — решение автора на грани, но не искажение фактов и не блокирует.
|
||||
|
||||
## 4. AC-по-AC разбор
|
||||
|
||||
### 4.1 Backend (AC2, AC3, AC4, AC5, AC11)
|
||||
|
||||
Читал `custom_components/houseplan/frontend_registration.py` целиком (659 строк) плюс
|
||||
`tests_backend/test_ha_frontend_registration.py` (24 теста) и `system_health.py`/`__init__.py`
|
||||
диффы. Параллельно второй читатель (независимый субагент) прошёл тот же материал —
|
||||
расхождений в выводах нет.
|
||||
|
||||
- **AC2 (честный outcome).** `async_register_lovelace_resource` (строки 191–265) различает все
|
||||
7 состояний; existing-запись обновляется без дубля (`_async_remove_duplicate_resources`,
|
||||
164–189, тест `test_registration_keeps_one_canonical_resource_and_removes_duplicates:114-130`);
|
||||
отсутствующий файл никогда не даёт успешный loader (`async_setup_frontend_registration:518-535,
|
||||
562-564`, тест `test_ha_setup.py::test_missing_frontend_bundle_does_not_skip_backend_setup:54-83`
|
||||
проверяет `resource_status == "not_attempted"`, `loader == "none"`). **Держится.**
|
||||
- **AC3 (retry без двойного loader).** `_schedule_retry` (475–504) всегда ставит фиксированную
|
||||
секундную задержку через `async_call_later` внутри колбэка `async_at_started`, даже когда HA уже
|
||||
running (в этом случае `async_at_started` вызывает колбэк синхронно, но `async_call_later`
|
||||
всё равно откладывает retry) — подтверждено и чтением, и тестом
|
||||
`test_already_running_setup_still_uses_the_fixed_one_second_delay:338-390`. Отмена
|
||||
start-listener/timer/retry-task привязана к `entry.async_on_unload(state.cancel)` (537),
|
||||
`cancel()` (92–105) корректно останавливает все три; все три окна отмены (до старта, во время
|
||||
задержки, во время задачи) явно проверены в `test_retry_lifecycle_handles_are_cancelled:456-551`
|
||||
тремя «поколениями» entry. Удаляется только собственный versioned URL setup'а
|
||||
(`_owned_fallback_urls`, 130–137 + `_remove_owned_fallback_url`, 304–327), чужой/пользовательский
|
||||
URL никогда не трогается (`test_preexisting_exact_fallback_is_used_but_never_claimed_or_removed:
|
||||
670-720`). Повторный вызов `_schedule_retry` из одного-единственного места (591) — «второй retry
|
||||
не наступает» верно по построению, а не только по тесту. **Держится.**
|
||||
- **AC4 (одноразовое notification).** Флаг пишется в `entry.data` только после того, как
|
||||
`persistent_notification.async_create` вернулся без исключения (`_async_create_reload_notice:
|
||||
350-433`), гонка «fallback-путь vs retry-завершение» серилизована `state._notice_lock` (360);
|
||||
тест `test_reload_notice_is_localized_and_persisted_once:723-770` дважды вызывает функцию и
|
||||
проверяет ровно одно создание. Missing file/static failure не расходуют флаг (365–372,
|
||||
`pending_frontend`). Uninstall dismiss-ит best-effort (631–634). **Держится.**
|
||||
- **AC5 (System Health).** `system_health.py:37-75` кладёт все восемь полей §10 поверх
|
||||
существующей статистики плана; параметризованный тест
|
||||
`test_system_health_reports_frontend_registration_matrix:1150-1335` покрывает
|
||||
missing/success/pending/yaml/exception и отдельно проверяет, что старые поля (`config_rev`,
|
||||
`spaces`, …) не исчезли (1314–1335). **Держится.**
|
||||
- **AC11 (downgrade/compatibility).** `websocket_api.py`, отдающий `integration_version`, этим
|
||||
диффом не тронут (нет в списке изменённых файлов); существующий `test_ha_websocket.py`
|
||||
не модифицирован. `test_ha_setup.py` получил один новый тест, остальные не изменены.
|
||||
**Держится** (проверено чтением, не повторным прогоном полного backend-набора — см. §6).
|
||||
|
||||
**Разрешённая неопределённость.** Backend-субагент отметил открытый вопрос: доступен ли
|
||||
`ConfigEntry.async_create_background_task` на минимально поддерживаемой HA 2024.6? Я скачал
|
||||
`homeassistant==2024.6.0` (`pip download`) и прочитал `homeassistant/config_entries.py` из wheel —
|
||||
метод существует с сигнатурой `(hass, target, name, eager_start=True)`, ровно как используется в
|
||||
`frontend_registration.py:494-497`. Вопрос закрыт, разрыва по минимальной версии нет.
|
||||
|
||||
`after_dependencies: ["lovelace"]` (`manifest.json:4-6`) — техническая избыточность поверх уже
|
||||
существующей жёсткой цепочки `houseplan → frontend → lovelace` (проверено: `dependencies` в
|
||||
манифесте houseplan уже содержит `frontend`, а `frontend`'s собственный манифест в
|
||||
HA 2026.8.3 содержит `lovelace` как hard dependency) — это defense-in-depth, не фикс
|
||||
несуществующего разрыва, и явно назван так в ТЗ §22 п.3 (не выдан за факт).
|
||||
|
||||
### 4.2 Frontend (AC6–AC10, AC12)
|
||||
|
||||
Читал `src/version-recovery.ts` и `src/version-recovery-card.ts` целиком, диффы
|
||||
`src/houseplan-card.ts`/`base.styles.ts`/i18n, три тестовых файла и
|
||||
`demo/smoke_version_recovery.mjs` полностью. Второй независимый читатель (субагент) прошёл тот же
|
||||
материал параллельно — расхождений нет.
|
||||
|
||||
- **AC6 (точное сравнение).** `normalizeRuntimeVersion`/`compareRuntimeVersions`
|
||||
(`version-recovery.ts:69-83`) трактуют не-строку/пустую/whitespace-строку как `unknown`, что
|
||||
стирает устаревший mismatch, а не оставляет его (мутант
|
||||
`version-recovery-treats-unknown-as-mismatch` прогнан лично мной — покраснел). Матрица
|
||||
§11.1 реализована в `_reconcile()` (233–260) построчно. **Держится.**
|
||||
- **AC7 (обычный режим никогда не reload сам).** Вне kiosk `_reconcile()` (242–247) никогда не
|
||||
вызывает `_armTimer()`; единственный путь к `reload()` — `renderVersionBanner`'s
|
||||
`event.isTrusted && controller.hasCurrentMismatchNotice` (`version-recovery-card.ts:195-197`).
|
||||
Мутант `version-recovery-auto-reloads-ordinary-view` прогнан лично мной — покраснел.
|
||||
**Держится.**
|
||||
- **AC8 (kiosk safety).** Все 10 полей `isVersionReloadSafe` (`version-recovery.ts:99-110`)
|
||||
замаплены на конкретное состояние карточки в `cardVersionReloadSafetySnapshot`
|
||||
(`version-recovery-card.ts:151-186`). `surfacesIdle` переиспользует существующий
|
||||
`_editorSecondaryDialogBlocked` (`houseplan-card.ts:9017-9027`, покрывает `_dangerConfirm`,
|
||||
`_importDialog`, `_backupImportDialog`, `_kioskDialog` и ещё 15 диалогов) плюс отдельно
|
||||
`_partitionDeleteDialog`/`_roomDeleteDialog`/`_backdropGuard`/`_vacFit`. `_openMoreInfo`
|
||||
(`houseplan-card.ts:5603-5619`) — единственная точка входа для всех вызовов (проверено: все
|
||||
5 call site — pointer/`_ctxDevice`, keyboard через `_clickDevice`, vacuum-иконка, info-card
|
||||
кнопки — сведены к этой функции), продлевающая `_cyclePausedUntil` безусловно при
|
||||
`this._kiosk`, независимо от модальности вызова. `_tick()` (278–306) перечитывает
|
||||
`hooks.safety()` заново на каждом 250-мс тике — не залипающий снапшот, TOCTOU-окна нет (проверка
|
||||
и `claimReloadTarget` происходят синхронно в одном тике). Четыре мутанта
|
||||
(`version-recovery-ignores-editor-state`, `-ignores-dialog-state`,
|
||||
`-ignores-pending-config-write`, `-ignores-interaction-pause`) прогнаны лично мной — все
|
||||
покраснели. **Держится.**
|
||||
- **AC9 (защита от reload-loop).** `attemptedTarget`/`claimReloadTarget`
|
||||
(`version-recovery.ts:117-147`) читают и пишут один и тот же ключ `sessionStorage`; target
|
||||
durable записан ДО вызова `reload()` (293–298, подтверждено мутантом
|
||||
`version-recovery-marks-target-after-reload` — покраснел лично у меня); сохранённый target
|
||||
уважается новым инстансом (мутант `version-recovery-ignores-stored-target` — покраснел лично у
|
||||
меня); недоступный `sessionStorage` — fail-safe в баннер (`attemptedTarget`/`claimReloadTarget`
|
||||
catch-блоки, 121-127/138-147). **Держится.**
|
||||
- **AC10 (граница full/space).** `test/version-recovery-card-contract.test.mjs:16-31` статически
|
||||
подтверждает отсутствие импорта version-recovery в `src/space-card.ts` (grep подтвердил
|
||||
0 совпадений); браузерно — `demo/smoke_version_recovery.mjs:349-362`
|
||||
(`staticOwnsNoController: !('_versionRecovery' in staticCard)`). **Держится.**
|
||||
- **AC12 (View/touch/a11y/визуальная стабильность).** `ordinaryStageDoesNotMove` (smoke) — баннер
|
||||
не двигает stage bbox; `role=status`/`aria-live=polite`/`aria-atomic=true` без повторного
|
||||
объявления благодаря early-return в `_showBanner` (`version-recovery.ts:308-310`); кнопка
|
||||
≥44×44 CSS px (`buttonMin44` в smoke); `prefers-reduced-motion` отключает анимацию
|
||||
(`_hideBanner`, 320-330 + `base.styles.ts`). Подавление тоста #353 — точное условие
|
||||
`info.terminal && this._versionRecovery.hasCurrentMismatchNotice`
|
||||
(`houseplan-card.ts:717,747`), не более широкое: non-terminal и terminal-без-баннера всегда
|
||||
показывают тост (проверено и unit-веткой, и smoke-сценариями
|
||||
`lazyToastWithBanner`/`terminalToastWithoutBanner`). Три golden-сценария (desktop/touch/kiosk
|
||||
after-attempt) приняты в `daccaf7e` со ссылкой на прогон CI. **Держится.**
|
||||
|
||||
### 4.3 Документация (AC1) и артефакты сборки (AC13)
|
||||
|
||||
`scripts/check-docs.mjs` (диф +72 строки) статическим парсером требует ровно два снепшота
|
||||
(`resource_mode: yaml` современный + помеченный legacy `mode: yaml`), запрещает плоский
|
||||
top-level `resources:`, и требует оба hard-reload shortcut во всех четырёх документах — сверил
|
||||
построчно с README.md/README.ru.md/USER-GUIDE.{md,ru.md}, текст идентичен ожидаемому контракту.
|
||||
Мутант `resource-docs-flatten-current-yaml` прогнан лично мной — покраснел. **Держится.**
|
||||
|
||||
AC13: три бандл-дерева (`dist`, `custom_components/.../frontend`, демо-стенд) синхронны — это
|
||||
часть зелёного Validate на `daccaf7e`, отдельно не перепроверял. i18n: en/ru/de/fr на фронтенде
|
||||
(`src/i18n/*.json`) и en/ru/de/fr на бэкенде (`strings.json`+`translations/*.json`) — сверил
|
||||
построчно, ключи присутствуют во всех восьми файлах без исключений.
|
||||
|
||||
## 5. Таблица «чем краснеет» (защитные AC, §2.7)
|
||||
|
||||
| AC | Чем доказан | Чем краснеет | Прогнал сам? |
|
||||
|---|---|---|---|
|
||||
| AC1 (docs contract) | `node scripts/check-docs.mjs` | мутант `resource-docs-flatten-current-yaml` (плоский `resources:` вместо вложенного) | Да — покраснел |
|
||||
| AC3 (retry без delay) | `pytest -k retry_waits_one_second` | мутант `frontend-registration-retries-without-delay` (снята фиксированная секундная задержка) | Нет (нет pytest/HA локально) — подтверждено чтением assertion `timer["delay"] == 1.0` дважды независимо (я + субагент) |
|
||||
| AC3 (retry вообще не наступает) | тот же тест | мутант `frontend-registration-skips-retry` (убран вызов `_schedule_retry`) | Нет — чтением: assertion `"callback" in started` |
|
||||
| AC3 (unload-race) | `pytest -k retry_lifecycle_handles_are_cancelled` | мутант `frontend-registration-is-not-unload-bound` (убран `entry.async_on_unload`) | Нет — чтением: assertion `len(before_start_entry.unload_callbacks) == 1` |
|
||||
| AC4 (флаг персистится) | `pytest -k reload_notice_is_localized_and_persisted_once` | мутант `frontend-reload-notice-forgets-persisted-flag` | Нет — чтением: assertion `len(created) == 1` после второго вызова |
|
||||
| AC6 (unknown не путается с mismatch) | `node --test --test-name-pattern="malformed values stay unknown" test/version-recovery.test.mjs` | мутант `version-recovery-treats-unknown-as-mismatch` | **Да — покраснел** |
|
||||
| AC6 (config/get владеет capability-adoption до соседних отказов) | `node demo/smoke_version_recovery.mjs` | мутант `version-recovery-delays-config-capability-adoption` | **Да — покраснел** (бандл пересобран, полный дорогой прогон) |
|
||||
| AC7 (обычный режим не reload) | `node --test --test-name-pattern="ordinary mode always" test/version-recovery.test.mjs` | мутант `version-recovery-auto-reloads-ordinary-view` | **Да — покраснел** |
|
||||
| AC8 (editor guard) | `test/version-recovery-card.test.mjs` (`card adapter maps editor state`) | мутант `version-recovery-ignores-editor-state` | **Да — покраснел** |
|
||||
| AC8 (dialog guard) | тот же файл (`maps blocking surfaces`) | мутант `version-recovery-ignores-dialog-state` | **Да — покраснел** |
|
||||
| AC8 (config-write guard) | тот же файл (`maps config writes`) | мутант `version-recovery-ignores-pending-config-write` | **Да — покраснел** |
|
||||
| AC8 (interaction-pause guard) | тот же файл (`maps interaction pause`) | мутант `version-recovery-ignores-interaction-pause` | **Да — покраснел** |
|
||||
| AC9 (target записан до reload) | `test/version-recovery.test.mjs` (`marks the exact target before one reload`) | мутант `version-recovery-marks-target-after-reload` | **Да — покраснел** |
|
||||
| AC9 (сохранённый target уважается) | тот же файл (`same target is once per tab`) | мутант `version-recovery-ignores-stored-target` | **Да — покраснел** |
|
||||
|
||||
10 из 14 мутантов прогнаны лично мной командой `node scripts/mutation-gate.mjs --id=<id>` —
|
||||
каждый показал `ok <id>: тест покраснел, как обязан`. 4 backend-мутанта относятся к дорогому
|
||||
гейту (`pytest`/`homeassistant`, недоступны в этой ревью-песочнице) — для них по правилу §2.7
|
||||
достаточно чтения: я лично прочитал целевые assertion'ы и подтвердил точное попадание в
|
||||
мутированную строку (см. §4.1); независимый второй читатель (субагент) пришёл к тем же выводам
|
||||
по тем же строкам.
|
||||
|
||||
## 6. Одно число — один источник
|
||||
|
||||
Версии, показанные в баннере (`version_mismatch.frontend`/`.backend`), и версия, используемая
|
||||
для ключа `sessionStorage`-guard'а, читаются из **одного** вызова:
|
||||
`compareRuntimeVersions(normalized.frontendVersion, normalized.backendVersion)`
|
||||
(`version-recovery.ts:215-222`), где `normalized.backendVersion` — результат
|
||||
`normalizeRuntimeVersion(capabilities.integration_version)` из того же `adoptCardConfigCapabilities`
|
||||
(`version-recovery-card.ts:52`), что и адаптирует capability. Второго независимого пути получения
|
||||
`integration_version` в новом коде нет — старое присвоение `_haIntegrationVersion` внутри
|
||||
`_reloadLayoutOnly()` (`houseplan-card.ts:4914-4917` до диффа читало `resp?.integration_version`
|
||||
из ответа `houseplan/layout/get`) **удалено этим диффом** (дифф `-4879,8 +4914,6` вычёркивает
|
||||
строки `this._haIntegrationVersion = typeof resp?.integration_version === 'string' ? ...`),
|
||||
то есть ранее существовавший второй источник (из `layout/get`) закрыт, а не оставлен рядом с
|
||||
новым (`config/get` через `adoptCardConfigCapabilities`). Один источник подтверждён.
|
||||
|
||||
## 7. Что проверено и признано корректным
|
||||
|
||||
- Все 13 AC ТЗ разобраны по коду (см. §4), для защитных — таблицей «чем краснеет» (§5).
|
||||
- Idempotency повторного `setup`/`reload`/`unload` — тестами `test_repeated_setup_updates_once_
|
||||
then_reuses_canonical_resource`, `test_fallback_survives_unload_reload_and_registry_
|
||||
reconciliation`, тремя «поколениями» в `test_retry_lifecycle_handles_are_cancelled`.
|
||||
- `_safe_error` (`frontend_registration.py:108-110`) — только `phase:ExceptionClassName`, ни
|
||||
сообщения, ни пути, ни стектрейса; проверено по всем вызовам (`async_register_lovelace_
|
||||
resource`, `_add_fallback`, `_remove_owned_fallback_url`, `async_setup_frontend_registration`).
|
||||
- Отсутствующий bundle не валит setup (`test_missing_frontend_bundle_does_not_skip_backend_
|
||||
setup`), sweep/repairs продолжают исполняться.
|
||||
- Uninstall убирает все записи с базовым URL, но никогда — чужой/пользовательский URL
|
||||
(`test_uninstall_removes_all_base_url_entries_fallback_and_notice`,
|
||||
`test_preexisting_exact_fallback_is_used_but_never_claimed_or_removed`).
|
||||
- `single_config_entry: true` в манифесте (не менялся этим диффом) исключает
|
||||
многократную конкурентную регистрацию из разных entries — предпосылка «второй retry не
|
||||
наступает» не имеет обходного пути через второй entry.
|
||||
- Бюджет бандла (`scripts/bundle-budget.mjs`) пересчитан и задокументирован: потолок
|
||||
298 500→301 000, бюджет 300 000→301 066, измеренный факт 300 090 Б gzip (+2 587 Б за
|
||||
eager version-recovery controller), 976 Б запаса; `test/bundle-assets.test.mjs` больше не
|
||||
хардкодит число, читает константу.
|
||||
- golden: 3 новых сценария (`version-mismatch-{desktop-dark-en,touch-light-ru,
|
||||
kiosk-attempted-dark-de}`) приняты на полном Linux CI-артефакте (`Baseline-Reviewed` ссылка
|
||||
в трейлере), `demo/golden/harness.mjs` до снятия кадра явно бросает, если целевая версия
|
||||
баннера/`sessionStorage` не совпала со сценарием — фейл-фаст на рассинхрон, а не тихий кадр.
|
||||
- i18n en/ru/de/fr полны на обеих сторонах (frontend `src/i18n/*.json`, backend
|
||||
`strings.json`+`translations/*.json`), `test/i18n.test.mjs` учитывает новый allowed-equal key.
|
||||
- Оба changelog обновлены в том же коммите, что и поведение (`fcf0ce80`, `User-Visible: yes`).
|
||||
- Все 16 коммитов диапазона несут `Issue: #462`; единственный `User-Visible: yes` коммит содержит
|
||||
правки в обоих changelog.
|
||||
|
||||
## 8. Чего не проверял и почему
|
||||
|
||||
- **Полный локальный прогон `tsc`/`npm test`/`npm run build`/`golden:verify`/
|
||||
`pytest tests_backend`** — не прогонял: они дешёвые по регламенту, но у меня уже есть зелёный
|
||||
Validate ровно на этом SHA (`daccaf7e`), и правило «дешёвые гейты уже подтверждены» разрешает их
|
||||
не перегонять в этом раунде.
|
||||
- **4 backend-мутанта (`frontend-registration-*`, `frontend-reload-notice-*`)** — не прогнаны:
|
||||
в этой ревью-песочнице нет `pytest`/`homeassistant` (`.venv-backend` не создаётся вне cloud-agent
|
||||
startup script, см. AGENTS.md), а мутация-гейт для них — дорогой (пересборка + pytest). По §2.7
|
||||
для дорогого backend-гейта разрешено читать assertion вместо повторного прогона — сделано дважды
|
||||
независимо (я и субагент), см. §5.
|
||||
- **97 из 98 «прямых совпадений» `smoke-select.mjs`** — не прогонял. Инструмент матчит по общим
|
||||
символам (`_mode`, `_zoom`, `_stageEl`, `_config`, …), но подавляющее большинство совпадений —
|
||||
побочный эффект того, что `VersionRecoveryCardPort` (`version-recovery-card.ts:71-136`) объявляет
|
||||
~40 УЖЕ существующих полей карточки как readonly-типы для предикатов безопасности, не меняя их
|
||||
семантику ни в одной строке. Ни один из этих смоков (decor/furniture/kiosk/resize/…) не проверяет
|
||||
код, который этот дифф действительно меняет — реальное изменение поведения ограничено
|
||||
`_openMoreInfo`, `_renderRoot`, `_syncVersionRecovery` и новыми модулями. Прогон всех 98 был бы
|
||||
предрелизным набором, а не гейтом ревью (PROCESS.md §8). Единственный по-настоящему целевой смок
|
||||
— `smoke_version_recovery.mjs` — прогнан (см. §2).
|
||||
- **`npm run invariants`** — не прогонял: diff не трогает геометрию, `layout`, `marker.space`,
|
||||
`open_spans` ни в одной строке.
|
||||
- **Performance-профили** — не прогонял: не названы в AC; единственное влияние на перф —
|
||||
бюджет бандла, который уже пересчитан и обоснован автором с измеренным фактом (см. §7).
|
||||
- **Ручное тестирование в браузере** — не проводилось (в этом процессе его нет вообще, код-ревью
|
||||
заменяет его, PROCESS.md §2.7).
|
||||
|
||||
## 9. Вывод
|
||||
|
||||
Все 13 AC либо доказаны автотестом, чью способность падать я лично подтвердил (10 мутантов из 14
|
||||
для #462 прогнаны лично, плюс независимая пара глаз на оставшихся 4), либо разобраны по коду с
|
||||
явной пометкой «проверено чтением» там, где повторный прогон дорогого гейта не входит в объём
|
||||
ревью. Три Low-находки не меняют вывод ни по одному AC и оставлены с записью. High и Medium
|
||||
находок нет — ни в скоупе, ни вне его.
|
||||
|
||||
**Вердикт: зелёный.**
|
||||
|
||||
---
|
||||
|
||||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||||
|
||||
## Материал раунда
|
||||
|
||||
- Ветка: `issue/462-card-resource-registration`, коммит `daccaf7ea54a` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||||
- Дерево материала: `0caccec714620782910b4f0c0fc60ddc0f769561`
|
||||
```
|
||||
git log --all --format='%H %T' | grep 0caccec71462
|
||||
```
|
||||
- ТЗ `docs/specs/462-card-resource-registration.md`, блоб `d3620eab48950255aa91240cd70ec3ed1edfd756`
|
||||
```
|
||||
git log --all --find-object=d3620eab48950255aa91240cd70ec3ed1edfd756 -- docs/specs/462-card-resource-registration.md
|
||||
```
|
||||
Reference in New Issue
Block a user