Issue: #561 User-Visible: no
16 KiB
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 вручную и вернул код обратно:
- 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 зелёный на точном SHAbb35af99(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, а не принято на слово автора. Находок нет.
Материал раунда
- Ветка:
issue/561-masonry-identity, коммитbb35af993a4d— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
195362973e09276e933bcc4b699e64a46cbb4b5dgit log --all --format='%H %T' | grep 195362973e09 - Тело issue:
9543698e33142ca2ceb66d906e271f5158a010ed8bfb00e873019519e5743970 - Вердикт конвейера:
green· High 0