docs: review document for #561
Проверка (CI) / Классификация изменённых файлов (push) Successful in 15s
Проверка (CI) / Мутанты по диффу (1/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (2/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (3/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (4/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (5/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Мутанты по диффу (6/6): затронутые свидетели краснеют (push) Skipped
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 32s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 14s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 16s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 35s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 5m5s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 6m26s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped

Issue: #561
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-12 23:17:47 +00:00
parent bb35af993a
commit c53f9ffc02
+188
View File
@@ -0,0 +1,188 @@
# CODE-REVIEW-561-r1
**Issue:** #561 «сохранить identity карточек Masonry после reload»
**Материал:** `bb35af993a4d5cc17376568155eefc83fc44822e` (единственный коммит на ветке `issue/561-masonry-identity`, `origin/dev..HEAD`)
**Заход:** r1 · блокирующих циклов израсходовано 0 из 4
**ТЗ:** тело issue #561, раздел `## ТЗ`; ревью ТЗ — `docs/reviews/SPEC-REVIEW-561-r2.md`, вердикт зелёный
## Скоуп
Один коммит, класс A (`src/summary-panel-identity.ts`, `src/summary-panel-runtime-loaded.ts`,
`src/summary-panel.ts`) + класс B (`scripts/mutation-gate.mjs`, тесты) + класс C
(`docs/ARCHITECTURE.md`, `docs/TESTING.md`, оба changelog) + класс D (синхронные бандлы
`dist/**`, `custom_components/houseplan/frontend/**`).
Продуктовая правка: identity сводной панели в native HA Masonry теперь строится из
канонического `hui-masonry-view.cards` (порядок конфигурации), а не из фактического
DOM-пути через визуальные `.column`. Для вложенных карточек (stack/conditional)
добавляется детерминированный composed-suffix. Пока canonical anchor не разрешён —
identity `null`, настройка работает только в сессии, localStorage не трогается.
Старые DOM-path ключи не мигрируются (Q1, зафиксировано в ТЗ).
## Как проверялось
### Прочитанное внешнее допущение — проверено, а не принято на веру
Реализация целиком опирается на утверждение «`hui-masonry-view.cards` — публичный
массив top-level card elements в исходном порядке `viewConfig.cards`, и Masonry только
перекладывает эти же элементы по визуальным колонкам, не трогая массив». Это прямо
названный риск в хендоффе автора. Проверил чтением закреплённого проектом исходника
HA frontend `20260729.7` (то же дерево, на которое ссылается S2-аналитика):
- `hui-masonry-view.ts`: `@property({ attribute: false }) public cards: HuiCard[] = [];`
— публичное свойство; `_createColumns()`/`_addCardToColumn()` берут элемент из
`this.cards[index]` и `appendChild` его в `columnEl` (или оборачивают в
`hui-card-options` для preview) — сам массив `cards` не переупорядочивается и не
фильтруется, меняется только физическое размещение уже существующих элементов.
- `hui-view.ts`: `_createCards()` строит `this._cards = config.cards.map(...)` — 1:1 с
`viewConfig.cards`, то есть с порядком конфигурации.
Допущение подтверждено чтением реального пиннутого источника, не только ссылкой на
номера строк из комментария автора.
### Ручная трассировка алгоритма (без исполнения)
Прошёл вручную `masonryPlacement`/`descendantPath` на nested-фикстуре теста
`#561 nested Masonry cards use a stable descendant suffix` (shadow-root между
`hui-vertical-stack-card` и внутренним div) и получил ту же строку, что утверждает
assert (`masonry-v2:0/shadow-root/div:0/houseplan-card:0`) — код читается корректно,
`childToken` правильно эмитит токен `shadow-root` на границе `getRootNode().host`.
### Автотесты — запущены лично, не только процитированы из хендоффа
```
npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs
node --test test/summary-panel.test.mjs test/summary-panel-runtime.test.mjs
```
→ 30/30 passed, включая все 6 новых `#561`-тестов и непереписанный `#437 placement
identity survives Masonry reflow and inner-card remount` (адаптирован под canonical
`cards`, семантика — та же: reflow/remount не меняют ключ).
```
node scripts/mutation-gate.mjs --id=summary-masonry-identity-uses-visual-dom-path
```
→ `ok чистый прогон` + `ok summary-masonry-identity-uses-visual-dom-path: тест
покраснел, как обязан` — AC7 подтверждён самим прогоном, не заявлением автора.
```
node scripts/no-new-any.mjs --base origin/dev --head HEAD
```
→ «Проверено добавленных строк в src/**/*.ts: 86 в 3 файл(ах). Новых any нет.»
```
node scripts/smoke-select.mjs --base origin/dev --head HEAD
```
→ НЕОПРЕДЕЛЁННОСТЬ (0 связанных смоков; символы вроде `masonryPlacement`,
`descendantPath` нигде не упоминаются смоками). Решение: не прогонять браузерный
смок в этом раунде — правка невизуальна (нет DOM/CSS/рендер-изменений, ТЗ §6 это
прямо утверждает), а production-shaped summary-panel smoke по AC8 явным решением
ТЗ вынесен на prerelease-гейт. Полная матрица — предрелизная обязанность.
### Собственная отрицательная проба AC4 (fail-closed), не заявленная автором
AC7-мутант в реестре покрывает только AC1/AC2/AC3 (визуальный DOM-путь вместо
canonical index). Отдельно защитный AC4 («unresolved never falls back») мутантом в
реестре не назван. Снял guard вручную и вернул код обратно:
```diff
- if (masonry.kind === 'unresolved') return null;
+ // mutant: unresolved falls back to structural path instead of fail-closed
```
Прогон `node --test --test-name-pattern="561" test/summary-panel.test.mjs
test/summary-panel-runtime.test.mjs` дал 2 красных из 5: `unresolved Masonry stays
session-only…` (ожидалось отсутствие get/set в localStorage) и `unresolved native
Masonry identity never falls back to a visual DOM path` (получил
`'hui-masonry-view/div:0/houseplan-card:0'` вместо `null`). Файл возвращён в исходное
состояние, `git status`/`git diff --stat` после отката — пусто, повторный чистый
прогон — 5/5 ok.
### Таблица «AC · чем доказан · чем краснеет» (§435)
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| AC1 full reload/rebalancing | `test/summary-panel.test.mjs`: `#561 full Masonry reload uses canonical cards…` (запущен лично) | мутант `summary-masonry-identity-uses-visual-dom-path` (запущен лично, PASS: тест краснеет) |
| AC2 одинаковые карточки | тот же тест (`Set` size 3, `notEqual` C/B) + `#561 identical Masonry cards reload their own local preferences` (рантайм, запущен лично) | тот же мутант — визуальный DOM-путь схлопывает индексы по колонке |
| AC3 живой reflow/remount/nested | `#437 placement identity survives…` + `#561 nested Masonry cards use a stable descendant suffix` (оба лично запущены) | тот же мутант для reflow-части; nested suffix — проверено чтением (трассировка выше) и тестом, покрывающим сам механизм `childToken`/`descendantPath` |
| AC4 fail-closed | `#561 unresolved Masonry stays session-only…`, `#561 unresolved native Masonry identity never falls back…` | собственная проба выше (guard снят вручную → оба теста красные, `null` заменился на структурный путь) |
| AC5 upgrade policy | `#561 unresolved Masonry stays session-only…` (старый ключ не читается/не трогается, новый берёт legacy-seed для масштабов и `show=false`) | без легаси-сида тест `assert.deepEqual(runtime.local, {…icon_scale:1.2, font_scale:0.9})` красит на дефолтах 1/1; без «не читать старый ключ» упал бы `assert.equal(browser.reads.includes(oldKey), false)` — оба ассерта реальны, тест прогнан лично |
| AC6 совместимость #437/#493 | fallback-ветка (`enclosingNativeCard`/`structuralPath`) байт-в-байт унаследована из до-#561 кода и гейтится только новой проверкой «нет ли `hui-masonry-view` в цепочке»; упражняется существующими `#493`-тестами в `summary-panel-runtime.test.mjs` (`hostFixture()` без masonry-предка) — прогнаны лично в составе тех же 30/30 | проверено чтением: ветка `not-masonry` не изменена диффом |
| AC7 mutation witness | сам мутант в `scripts/mutation-gate.mjs`, id `summary-masonry-identity-uses-visual-dom-path` | запущен лично (см. выше), 1 из 1 поймано |
| AC8 стандартные гейты | Validate green на `bb35af99`: https://github.com/Matysh/houseplan-card/actions/runs/34724545711 (typecheck/test/build) + лично `no-new-any`, `mutation-gate --id=…`, `check-docs` (warn-режим — как на обычном push) | — (не защитный AC, обычная сверка) |
## Что проверено и корректно
- Ветка/трейлеры: один коммит, `Issue: #561`, `User-Visible: yes`, оба changelog
(`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) правлены в этом же коммите.
- `docs/ARCHITECTURE.md` и `docs/TESTING.md` описывают новый резолвер и границы
проверки — по существу и без противоречий коду.
- `summaryLocalKey`/`preferenceKey`/`loadLocal`/`saveLocal` в
`summary-panel-runtime-loaded.ts` корректно превращают `slot: null` в
`key: null`, что гейтит и `getItem`, и `setItem` (строки 408-409, 428-432) —
подтверждено и чтением, и тестами выше.
- Единственная точка вызова `stableSummaryPlacementSlot` — `placementSlot()`,
сигнатура `string | null` пробрасывается корректно, `tsc` (в составе Validate)
зелёный.
- Кеш `placementSlots` не маскирует «unresolved»: `masonryPlacement` пишет в
`WeakMap` только на ветке `resolved`, поэтому повторный вызов после появления
`cards` действительно пересчитывает identity — соответствует ТЗ п.3.6.
- Скоуп не расширен: новый публичный `card_id`, миграция хранения, перенос после
reorder — не введены; ровно то, что запрещало ТЗ.
- «Одно число — один источник»: не применимо, диффе не добавляет и не дублирует
пользовательскую величину (identity — не отображаемое число).
- Геометрия/`layout`/толщины стен не затронуты — `model-invariants` не требуется.
- Python-бэкенд не затронут — `pytest tests_backend` не требуется.
- Визуальных изменений нет (ТЗ §6 прямо это утверждает, диффом подтверждено —
ни CSS, ни шаблонов, ни рендера) — `golden:verify` не требуется.
## Находки
Нет. High: 0, Medium: 0, Low: 0.
## Чего не проверял и почему
- Полный `npm test`/`npx tsc --noEmit`/`npm run build` со сверкой трёх копий
бандла — не перегонял: Validate зелёный на точном SHA `bb35af99`
(https://github.com/Matysh/houseplan-card/actions/runs/34724545711), это дешёвые
гейты и они уже сошлись на этом прогоне. Отдельно перепроверил только
`no-new-any` и `mutation-gate --id=…`, так как они дёшевы и напрямую относятся
к защитным AC7.
- Полная матрица браузерных смоков (`demo/smoke_*.mjs`) — не прогонял; выборка
`scripts/smoke-select.mjs` вернула НЕОПРЕДЕЛЁННОСТЬ, а ТЗ AC8 явно относит
production-shaped summary-panel smoke (включая touch/kiosk viewport rotation)
на prerelease-гейт. Решение принято на этапе ТЗ (зелёное ревью ТЗ), а не мной
сейчас задним числом.
- `npm run golden:verify`, `python -m pytest tests_backend`,
`node scripts/model-invariants.mjs` — не запускал: диф не меняет визуальный
результат, геометрию/ссылки на неё или Python-код.
- Полная пересъёмка `check-docs`/`docs:capture` в строгом режиме — не делал:
Validate использует `--screenshots=warn` на обычном push (heavy=false), строгий
режим — обязанность prerelease-гейта (#479); зелёный Validate на этом SHA уже
подтвердил обычный (warn) прогон check-docs.
## Материал раунда
- SHA: `bb35af993a4d5cc17376568155eefc83fc44822e`
- Диапазон: `origin/dev..HEAD` — один коммит
- Рабочая копия после всех проб (`git status --porcelain`) — чисто, HEAD не менялся
## Вердикт
Зелёный. Все AC1–AC8 доказаны автотестами, которые я лично прогнал и для двух
защитных (AC4, AC7) — лично сломал и восстановил. Внешнее допущение о контракте
`hui-masonry-view.cards` проверено чтением реального пиннутого исходника HA
frontend, а не принято на слово автора. Находок нет.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/561-masonry-identity`, коммит `bb35af993a4d` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `195362973e09276e933bcc4b699e64a46cbb4b5d`
```
git log --all --format='%H %T' | grep 195362973e09
```
- Тело issue: `9543698e33142ca2ceb66d906e271f5158a010ed8bfb00e873019519e5743970`
- Вердикт конвейера: `green` · High 0