Files
houseplan-card/docs/reviews/CODE-REVIEW-561-r1.md
T
claude[bot] c53f9ffc02
Проверка (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
docs: review document for #561
Issue: #561
User-Visible: no
2026-09-12 23:17:47 +00:00

16 KiB
Raw Blame History

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 зелёный на точном 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, а не принято на слово автора. Находок нет.


Материал раунда

  • Ветка: issue/561-masonry-identity, коммит bb35af993a4d — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 195362973e09276e933bcc4b699e64a46cbb4b5d
    git log --all --format='%H %T' | grep 195362973e09
    
  • Тело issue: 9543698e33142ca2ceb66d906e271f5158a010ed8bfb00e873019519e5743970
  • Вердикт конвейера: green · High 0