mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
committed by
Sergey Matyunin
parent
a43bc3eb27
commit
87f19941a3
@@ -0,0 +1,219 @@
|
||||
# CODE-REVIEW-490-r1
|
||||
|
||||
- **Issue:** #490 — Атомарный recovery и live-состояние сводной панели
|
||||
- **Этап:** code (PROCESS.md §2.7)
|
||||
- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4
|
||||
- **SHA реализации:** `c6e6660d483c9297fc5fa86e9f9b5a7ad82a85a5` (ветка `issue/490-summary-recovery-live-state`)
|
||||
- **ТЗ:** `docs/specs/490-summary-recovery-live-state.md`, ревью ТЗ зелёное на `95a34f73` (см. комментарий issue)
|
||||
- **Диапазон:** `git diff origin/dev...HEAD` (57 файлов, 1179+/429-), из них продуктовый код:
|
||||
`src/summary-panel.ts`, `src/summary-panel-runtime-loaded.ts`, `src/summary-panel-host.ts`,
|
||||
`src/houseplan-card.ts` (2 строки), тесты `test/summary-panel.test.mjs`,
|
||||
`test/render-device-snapshot.test.mjs`, новый `demo/smoke_summary_panel.mjs` (recovery/live
|
||||
сценарии добавлены к существующему UI-смоку), `scripts/smoke-links.mjs`, changelog x2, dist/бандлы.
|
||||
|
||||
## Скоуп
|
||||
|
||||
Задача устраняет F1 (lost-ACK recovery устанавливал только `rev`, но старый `candidate`) и
|
||||
F2 (entity-источники сводной панели не входили в render dependency set). Это единственная
|
||||
задача в этом заходе; ТЗ уже прошло отдельное зелёное ревью, здесь оценивается реализация
|
||||
против AC1–AC6.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
Зелёного Validate на `c6e6660d` не нашёл (прогон не завершён/не найден), поэтому дешёвые гейты
|
||||
прогнал сам:
|
||||
|
||||
| Гейт | Результат |
|
||||
|---|---|
|
||||
| `npx tsc --noEmit` | green, без ошибок |
|
||||
| `npm test` | green — 2276 pass, 0 fail, 1 skip (Node 22.23.2, Linux) |
|
||||
| `npm run build` + сверка dist/custom_components/demo-srv | green — рабочее дерево после build/`node scripts/bundle-sync.mjs` осталось чистым (без diff к закоммиченным трём копиям бандла) |
|
||||
| `node scripts/check-docs.mjs` | ERROR: «screenshot source fingerprint is stale ... (#479)» — это известный, помеченный самим инструментом долг #479 (отпечаток считается по всему `src/**`, любая фронтенд-правка делает его устаревшим); не регрессия этой задачи, отдельно не блокирую |
|
||||
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | прогнал вывод, см. ниже |
|
||||
| `node demo/smoke_summary_panel.mjs` | green (`OK`) — включает новые recovery/live сценарии |
|
||||
| `node demo/smoke_render_invalidation.mjs` | green — прямое совпадение по `entityIds` |
|
||||
| `node demo/smoke_device_position_history.mjs` | green — прямое совпадение по `_adoptStructuralResponses` |
|
||||
| `node demo/smoke_danger_confirmation.mjs` | green — прямое совпадение по `_adoptStructuralResponses` |
|
||||
| `npm run invariants` | не запускал — diff не трогает рёбра/толщину/`layout`/`marker.space`/`open_spans` |
|
||||
| `python -m pytest tests_backend` | не запускал — `custom_components/**/*.py` не тронут (только фронтенд-бандл) |
|
||||
| golden/performance полные наборы | не запускал — это предрелизный гейт (PROCESS.md §8), diff не меняет геометрию/стили/слои рендера, только текстовые значения существующего блока |
|
||||
|
||||
`smoke-select` дал 22 «прямых совпадения» и 58 «слабых связей» (общее имя `_maybeRebuildDevices`/`_model`/`_cfgRev` — решение ревьюера). Прогнал те прямые совпадения, что относятся к затронутым сиротным символам (`entityIds`, `_adoptStructuralResponses`); остальные прямые совпадения (`_cfgContentFingerprint`, `_restoreZoom`, `SpaceModel` и т.д.) относятся к путям, которые эта задача не меняла (сами функции идентичны на `origin/dev`, изменился только один новый вызывающий), и уже покрыты общим `npm test`/`tsc`.
|
||||
|
||||
Читал код (не исполнял отдельно) `_adoptStructuralResponses`, `_beginContinuityCandidate`,
|
||||
`VisualContinuityController` (`src/visual-continuity.ts`), `ContentSigner.prepareImage/display`
|
||||
(`src/signing.ts`), три существующих вызывающих места adoption-seam (`_loadFromServer`,
|
||||
`_reloadConfigOnly`, `space-card.ts:718`) — чтобы сравнить их с новым, четвёртым, в
|
||||
`summary-panel-runtime-loaded.ts`.
|
||||
|
||||
## Находки
|
||||
|
||||
### M1 — Recovery-адаптация обходит asset-readiness gate и continuity-overlay (в скоупе, Medium)
|
||||
|
||||
`src/summary-panel-runtime-loaded.ts:439-452` (метод `saveDialog()`, ветка lost-ACK):
|
||||
|
||||
```ts
|
||||
const authoritative = await this.host._getAuthoritativeConfig();
|
||||
const confirmed = confirmedSummaryPanelWriteRecovery(authoritative, draft);
|
||||
if (!confirmed) throw writeError;
|
||||
const visibleSpace = this.host._space;
|
||||
this.host._adoptStructuralResponses(authoritative);
|
||||
void this.host._syncDecorAssets(confirmed.config).catch(() => undefined);
|
||||
this.host._adoptInitialSpace(this.host._model, true);
|
||||
this.host._cacheSnapshot();
|
||||
if (this.host._space !== visibleSpace) this.host._restoreZoom();
|
||||
this.host._regSignature = '';
|
||||
this.host._maybeRebuildDevices();
|
||||
recovered = true;
|
||||
```
|
||||
|
||||
Это четвёртый вызывающий `_adoptStructuralResponses` в кодовой базе. Три существующих
|
||||
(`houseplan-card.ts:4286` `_loadFromServer`, `houseplan-card.ts:4467` `_reloadConfigOnly`,
|
||||
`space-card.ts:718`) все делают одно и то же перед вызовом adoption-seam:
|
||||
|
||||
```ts
|
||||
if (configChanged && !await this._signer.prepareImage(
|
||||
this.hass, this._candidateBackdrop(candidateConfig),
|
||||
)) {
|
||||
this._continuity.note('asset-failed');
|
||||
this._scheduleLoadRetry(true);
|
||||
return; // старый _serverCfg остаётся на экране, пока фон не готов
|
||||
}
|
||||
```
|
||||
|
||||
`ContentSigner.prepareImage` — «Sign and decode a protected image before a structural
|
||||
candidate adopts it. The current frame can therefore keep its old backdrop for the whole
|
||||
wait» (`src/signing.ts:350-353`). Новый путь в `saveDialog()` этот шаг не делает и не
|
||||
проверяет `configChanged` вовсе — `_adoptStructuralResponses` вызывается безусловно.
|
||||
Он также не вызывает `_beginContinuityCandidate(...)` (recovery-overlay, #73/#451) и не
|
||||
вызывает `_resumePendingNavMode()`, которые есть во всех трёх других местах.
|
||||
|
||||
**Почему это задевает именно эту задачу.** ТЗ §6.2 прямо требует: «Если authoritative
|
||||
adoption обнаруживает новую структурную базу, сохраняются действующие правила сброса stale
|
||||
history/drag/cache и continuity #73/#451. Recovery панели не получает собственного обходного
|
||||
варианта этих правил.» История/drag/cache сброс действительно сохранён — он находится внутри
|
||||
самого `_adoptStructuralResponses` (geometryHistory, devicePositionHistory, drag, camera —
|
||||
проверил чтением, это общий код, использованный без изменений). Но asset-readiness gate и
|
||||
recovery-overlay — тоже часть тех же continuity-правил (#73/#451, тот же файл
|
||||
`visual-continuity.ts`, та же авторская формулировка «keep its old backdrop for the whole
|
||||
wait») — и именно по ним recovery панели завела свой обходной вариант: единственный из
|
||||
четырёх вызывающих, который не ждёт `prepareImage`.
|
||||
|
||||
**Сценарий отказа.** Клиент A открывает диалог сводной панели, сохраняет. Ответ на
|
||||
`config/set` теряется по транспорту после того, как сервер его принял. Параллельно клиент B
|
||||
успевает сохранить несвязанную структурную правку — например, заменить фоновое изображение
|
||||
текущего пространства (`spaces[i].bg.href`) — именно такой параллельный несвязанный write
|
||||
и есть предмет AC1/AC2. Recovery клиента A подтверждает lost-ACK (`confirmedSummaryPanelWriteRecovery`
|
||||
проверяет только совпадение `summary_panel`, остальная часть документа — включая `bg.href` —
|
||||
принимается «как есть») и вызывает `_adoptStructuralResponses(authoritative)` напрямую.
|
||||
Новый `bg.href` ещё не подписан (`ContentSigner.signed` не содержит записи для нового пути) —
|
||||
`_display()` для непобранного URL возвращает `''` («a signature we hold and still trust, else
|
||||
nothing», `signing.ts:161-179`), поэтому фон плана на один кадр/до ближайшего подписания
|
||||
пропадает (пустой src), вместо того чтобы держать старый фон, как это гарантируют три других
|
||||
пути. Само подписание триггерится тем же вызовом `_display()` и самовосстанавливается на
|
||||
следующем цикле — это не постоянная поломка, а видимый, но переходный дефект рендера именно
|
||||
в момент, который ТЗ описывает как основной (recovery при параллельной структурной правке).
|
||||
|
||||
**Проверено чтением, не исполнением** — воспроизвести смоком дорого (нужен реальный
|
||||
`ContentSigner`/подписанный URL в browser harness), а `demo/smoke_summary_panel.mjs` меняет
|
||||
только `spaces[0].title` и `settings.concurrent_guard_490` — ни то, ни другое не задевает
|
||||
`ContentSigner`, поэтому текущий смок этот путь не покрывает и не может его поймать.
|
||||
|
||||
**Серьёзность:** Medium, в скоупе задачи (сам предмет AC1/AC2 — атомарное принятие «всего»
|
||||
документа при recovery, включая структурные поля вроде фона). Не блокирует (не High): не
|
||||
ломает данные и не искажает `summary_panel`, эффект переходный и самовосстанавливающийся,
|
||||
требует узкого стечения (lost ACK + именно в этом окне конкурентная правка фона). Чинится
|
||||
добавлением `configChanged`-гейта на `prepareImage` (и, по аналогии с тремя другими местами,
|
||||
`_beginContinuityCandidate`) перед вызовом `_adoptStructuralResponses` в этой ветке —
|
||||
без выдумывания нового поведения, тем же способом, что и в трёх существующих вызывающих.
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- **AC1/AC2 (атомарность и следующая запись).** `confirmedSummaryPanelWriteRecovery`
|
||||
(`src/summary-panel.ts`) требует одновременно: полный `ServerConfig` (`Array.isArray(config.spaces)`),
|
||||
числовой неотрицательный safe-integer `rev`, точное совпадение сохранённой `summary_panel`
|
||||
с draft (`sameSummaryPanel`, JSON-эквивалентность). При выполнении — весь `authoritative`
|
||||
документ и `rev` уходят через общий `_adoptStructuralResponses`, а не собираются вручную —
|
||||
это закрывает риск «принят только rev или только config» из ТЗ §11. Подтверждено и unit-тестом
|
||||
(`test/summary-panel.test.mjs`, матрица из 7 отрицательных и 1 положительного случая) и
|
||||
browser-смоком `demo/smoke_summary_panel.mjs` (`recoveryAdoptsWholeConfig`,
|
||||
`recoveryAdoptsRevision`, `nextWriteUsesRecoveredRevision`,
|
||||
`nextWritePreservesConcurrentChange`) — прогнал сам, green. Тест умеет падать: ассерты
|
||||
сверяют конкретные строки (`'Concurrent title kept'`, `'kept'`) и конкретную revision-арифметику
|
||||
(`writes[0].expected_rev + 2`), а не факт отсутствия исключения.
|
||||
- **AC3 (настоящий конфликт).** Ветка `catch { throw writeError; }` вокруг recovery-попытки —
|
||||
любой сбой самого чтения (`_getAuthoritativeConfig` бросает) или отказ `confirmedSummaryPanelWriteRecovery`
|
||||
(панель отличается / config или rev невалидны) откатывается к исходной ошибке записи, диалог
|
||||
не закрывается. Смок `trueConflictStaysOpen` (принудительный `rejectConflict = true` на каждой
|
||||
попытке) — green, и явно проверяет, что предыдущая («Second summary write») сохранённая версия
|
||||
осталась в `_serverCfg`, а не смешалась со stale candidate.
|
||||
- **AC4/AC5 (live-источники и fast path).** `summaryPanelEntityIds()` — чистая функция, не
|
||||
читает `hass`, не мутирует конфиг, схлопывает дубликаты, режется по `runnableSummaryPanel`
|
||||
(только поддерживаемая v1), игнорирует локальную видимость/scope, что соответствует ТЗ §7.1
|
||||
дословно («число ограничено схемой» — до 200 значений по `SUMMARY_PANEL_SCHEMA`, что и
|
||||
проверяет unit-тест на hidden/other-scope/system/unsupported случаях). В
|
||||
`houseplan-card.ts:4623` результат добавлен в тот же `entityIds` render-snapshot набор, что и
|
||||
устройства/комнаты/декор — второго listener/timer не создано (проверено чтением всего файла
|
||||
на предмет других мест, где могла бы завестись отдельная подписка). Live-переходы value →
|
||||
unavailable → recovered → missing → present и счётчики рендера/geometry-инвариантность
|
||||
проверены смоком (`liveState` секция) — green, включая `unrelatedTickSkipped: true`,
|
||||
`relevantTickRenderedOnce: true`, `stateTicksKeepGeometry: true`.
|
||||
- **Кеш-инвалидация снапшота.** `_capturedSnapshotSequence = -1` добавлен именно в момент
|
||||
ленивой загрузки `LoadedSummaryPanelRuntime` (`connectedCallback`, `houseplan-card.ts:2613`) —
|
||||
без него уже захваченный снапшот (`_hassSequence` не поменялся) не подхватил бы вновь
|
||||
появившиеся `entityIds()` до следующего изменения hass. В самом recovery-пути отдельный сброс
|
||||
не нужен: `_adoptStructuralResponses` переприсваивает `_serverCfg` новой ссылкой при
|
||||
structural-change, Lit-реактивность ловит это в `willUpdate` (`houseplan-card.ts:4084-4088`) и
|
||||
инкрементирует `_cfgEpoch`, что и инвалидирует кеш снапшота обычным путём — проверено чтением.
|
||||
- **AC6 (совместимость).** Диф не трогает EN/RU/DE/FR словари, не добавляет новых сетевых
|
||||
запросов в steady state (единственный новый `config/get` — тот же самый вызов, что и раньше,
|
||||
просто с другим следствием), не меняет схему/API. `test/render-device-snapshot.test.mjs`
|
||||
подтверждает, что `_summary?.entityIds()` вошёл именно в существующую сборку снапшота, а не
|
||||
завёл параллельную.
|
||||
- **Трейлеры и changelog.** `Issue: #490` на всех трёх коммитах, `User-Visible: yes` на
|
||||
коммите реализации `c6e6660d`, и `docs/CHANGELOG.md`+`docs/CHANGELOG.ru.md` правлены в этом
|
||||
же коммите (не отдельным) — соответствует требованию.
|
||||
- **Единственный источник числа.** Значение summary-only сущности показывается только в одной
|
||||
строке сводной панели; диф не вводит второе место, где то же значение показывалось бы снова
|
||||
(никакой другой слой плана не подписан на источники, которых нет в общем `entityIds`) — не
|
||||
вижу дублирующего числа, которое требовало бы отдельного разбора по «одно число — один
|
||||
источник».
|
||||
|
||||
## Что не проверял
|
||||
|
||||
- Полный `golden:verify` — diff не меняет геометрию/раскладку/стили, только текстовые
|
||||
значения существующего блока и внутреннюю логику recovery; посчитал непропорциональным для
|
||||
этой правки.
|
||||
- Windows-специфичные 3 падения `npm test`, о которых пишет автор (#479) — не переисполнял на
|
||||
Windows, полагаюсь на то, что Linux-прогон (канонический CI job) у меня самого дал 0 fail.
|
||||
- `npm run invariants` — diff не трогает геометрию/рёбра/`layout`/`marker.space`/`open_spans`.
|
||||
- `python -m pytest tests_backend` — `custom_components/**/*.py` не в diff (только
|
||||
сгенерированный фронтенд-бандл под `custom_components/houseplan/frontend/`).
|
||||
- Полную матрицу `demo/smoke_*.mjs` (232 файла) — прогнал только прямые совпадения, относящиеся
|
||||
к реально изменённым символам (`entityIds`, `_adoptStructuralResponses`); остальные 20 «прямых»
|
||||
и все «слабые» совпадения относятся к функциям, чьи тела не менялись в этом diff (только один
|
||||
новый вызывающий), и уже покрыты `tsc`+`npm test`+`build`.
|
||||
- Живое ручное тестирование в браузере HA — недоступно в этом цикле; вывод построен на
|
||||
автотестах и чтении кода.
|
||||
|
||||
## Вердикт
|
||||
|
||||
Жёлтый: единственная находка (M1) — Medium в скоупе задачи, чинится в этой же ветке без
|
||||
отдельного issue (решение владельца 2026-08-19, #202). AC1–AC6 в остальном подтверждены
|
||||
тестами, которые умеют падать, и чтением кода там, где смок не достаёт (сам M1).
|
||||
|
||||
---
|
||||
|
||||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||||
|
||||
## Материал раунда
|
||||
|
||||
- Ветка: `issue/490-summary-recovery-live-state`, коммит `c6e6660d483c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||||
- Дерево материала: `fc7364e6d21b43b3f08c757a8ce0d5d5ac9b48c2`
|
||||
```
|
||||
git log --all --format='%H %T' | grep fc7364e6d21b
|
||||
```
|
||||
- ТЗ `docs/specs/490-summary-recovery-live-state.md`, блоб `d4bfd4ea63f1b2ce1cb255b2764fe14d25661df7`
|
||||
```
|
||||
git log --all --find-object=d4bfd4ea63f1b2ce1cb255b2764fe14d25661df7 -- docs/specs/490-summary-recovery-live-state.md
|
||||
```
|
||||
Reference in New Issue
Block a user