Files
houseplan-card/docs/reviews/SPEC-REVIEW-613-r2.md
T
2026-09-23 02:25:10 +00:00

235 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC-REVIEW-613-r2
Материал: тело issue #613, редакция после правки автора 2026-09-23T02:14:56Z
(последняя `userContentEdit`, применена через 88 секунд после вердикта r1,
опубликованного 2026-09-23T02:13:28Z). Хэш тела на момент этого ревью:
`sha256:71ea7980f0a9d84820d118ab5bcc6e7fa71f7e0be9368498f58a4c30c197af5e`
(вычислен с `\r\n` как в живом API; без завершающего перевода строки —
`1115bb4c836ca6529c546e6d13af5dc515be93553a8b6c468a2e3d6f47dfac89`). Дерево
кода на момент проверки — `d36b7f80c3208eab7fdaca6fdfd6c4d131a86cba` (рабочая
копия); код относительно материала r1 (`1241b9499c7b`) не менялся — единственный
коммит между раундами добавил только `docs/reviews/SPEC-REVIEW-613-r1.md`
(подтверждено: `git diff 1241b949..d36b7f80 --stat -- src/ demo/ scripts/`
пуст). Код читан только для проверки утверждений ТЗ, не как материал
код-ревью, и в этом раунде повторно не читался — см. «Унаследовано из r1».
Заход r2, блокирующих циклов израсходовано 1 из 4 (r1 — жёлтый, потратил 1;
зелёный цикла не образует и бюджет не тратит, §4/#227).
**Примечание к процессу поиска материала.** Вердикт r1 в комментарии issue не
называет SHA явно (только «Документ: docs/reviews/SPEC-REVIEW-613-r1.md») —
это соответствует описанному в задании шаблону «SHA в вердикте не назван».
SHA и весь материал раунда найдены в блоке «Материал раунда» в конце самого
документа `SPEC-REVIEW-613-r1.md` (машинный блок, подставлен конвейером).
Дополнительно раунд r1 не назвал SHA/blob тела issue *до* правки в виде,
удобном для машинной сверки, — этот пробел закрыт вручную через GraphQL
`userContentEdits` (три исторические редакции тела; см. ниже), поскольку
`docs/specs/*.md` для issue #613 не существует (задача заведена после
2026-09-10, ТЗ живёт только в теле issue, архив не создаётся — верно по
`PROCESS.md` §7.3 п.1).
## Скоуп ревью
Раунд r2 разбирает **только дельту тела issue между материалом r1 и текущим
состоянием** — это локальная точечная правка (переформулировка технического
решения и AC в ответ на H1/M1 r1), без ребейза, без смены подсистемы, без
смены поведенческого контракта в остальных разделах. Условия «разбор остаётся
полным» (ребейз на ушедший вперёд `dev`, смена контракта, новая подсистема,
объём дельты сопоставим с исходной задачей) не выполнены ни одно — код не
менялся вообще, а дельта текста ограничена 6 из 22 разделов ТЗ. Полный
повторный разбор всего ТЗ не требуется; см. «Унаследовано из r1» для
остального.
Проверялось в этом раунде:
- дельта тела issue между материалом r1 (`sha256`-снимок редакции от
2026-09-23T02:03:28Z, 9278 байт) и текущим состоянием (10685 байт, 2026-09-23T02:14:56Z),
раздел за разделом;
- закрывает ли новая формулировка H1 (нерабочее предположение о
`scroll`-инвалидации через `ownerDocument`) и M1 (AC1/AC2 не требовали
witness за пределами `document`-дерева карточки) технически, а не
косметически;
- не породила ли переформулировка новое противоречие «Контракт ↔ Риски»,
аналогичное найденному в r1;
- не разошлись ли изменённые AC (AC1, AC2) с неизменными соседними AC
(AC3–AC6) и с планом автотестов.
## Как проверялось
- Получено тело issue #613 текущей редакции через `gh issue view --json body`.
- Получена полная история редактирования тела через GraphQL
`repository.issue.userContentEdits` (3 записи: исходный короткий баг-репорт
19:54:25Z → полное ТЗ 02:03:28Z, которое и было материалом r1 → финальная
правка 02:14:56Z, материал r2). Снимок 02:03:28Z побайтово сверен с цитатами
в `docs/reviews/SPEC-REVIEW-613-r1.md` (раздел «Находки», H1/M1) —
совпадает дословно, значит найден правильный «до»-снимок, а не соседняя
редакция.
- Тело до/после разбито на секции по заголовкам `##`/`###`; секции сравнены
попарно (`old[k] != new[k]`) — изменившимися оказались ровно 6 из 22:
«Скоуп», «Контракт поведения», «Критерии приёмки», «План автотестов»,
«Риски», «Принятые технические предположения». Остальные 16 (Сценарий, Что
человек увидит, Проблема, Не-скоуп, UX, Модель данных и миграция, i18n, AC5/AC3/AC4/AC6
как текст, Производительность, Touch, Откат, Release-артефакты и вводные
абзацы «Подтверждённые дефекты») побайтово идентичны материалу r1.
- Прочитан полностью PROCESS.md §2.4, §2.9 (правильное название — фактически
§2.10 в файле, «Повторный раунд ревью — объём по дельте»), §4, §7.2 —
сверены правила бюджета циклов, формат вердикта и обязательные разделы
документа для повторного раунда.
- Прочитан `docs/TOUCH-SUPPORT.md` на предмет упоминаний scroll/shadow —
канонический документ не фиксирует конкретный механизм инвалидации, новое
техническое решение ему не противоречит (поиск `shadow|scroll|composed` —
два несвязанных совпадения, оба про другой контекст).
- Автотесты/гейты не гонялись: стадия `spec`, код не менялся между раундами
(проверено `git diff 1241b949..d36b7f80 --stat -- src/ demo/ scripts/` —
пусто), гонять `tsc`/`test`/`build` не на чем и незачем — эти гейты относятся
к этапу `code`, не к этапу `spec`.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **H1 (High).** Предположение «capture-listener `scroll` на `ownerDocument` + `visualViewport`» не работает, если реальный scroll-предок карточки находится в чужом shadow-root (событие `scroll` не `composed`, подтверждено playwright-экспериментом в r1) — противоречие с разделом «Риски» не разрешалось. | Механизм заменён: раздел «Принятые технические предположения» теперь описывает **обход composed ancestor-chain** (`parentElement`, затем `ShadowRoot.host`, до document/window) с прямой подпиской на каждый источник — то есть выбран вариант (а) из требования r1 («подъём по реальной цепочке скроллящихся предков через `getRootNode()`»), а не документированный компромисс (б). Раздел «Риски» больше не содержит противоречащего утверждения «слушатель только на `window` не покрывает все случаи» — эта фраза удалена, вместо неё — риск нового рода («composed-цепочка может измениться после remount»), не конфликтующий с контрактом. | Тело issue, раздел «Скоуп» (буллет 1–2, переформулирован под composed-цепочку), раздел «Контракт поведения» п.2 (переписан целиком: «`scroll` не считается `composed`... обходит composed-цепочку вверх... Поэтому scroll-контейнер в любом внешнем shadow-root инвалидирует кэш»), раздел «Принятые технические предположения» (первый пункт заменён целиком на lifecycle-helper по composed ancestor-chain), раздел «Риски» (второй пункт заменён). |
| **M1 (Medium, в скоупе).** AC1/AC2 не требовали, чтобы хотя бы один smoke-прогон воспроизводил scroll-контейнер **за пределами `ownerDocument`-дерева карточки** (внутри стороннего `attachShadow`) — иначе light-DOM смок мог «доказать» AC и не поймать реальный дефект HA-вложения. | AC1 переименован в «scroll hit correctness **across Shadow DOM**» и теперь явно требует: «карточка помещена внутрь прокручиваемого контейнера во **внешнем** `attachShadow`... прокрутки **внешнего контейнера**... Дополнительный light-DOM вариант допустим, **но не заменяет этот witness**». AC2 добавляет «включая ancestor **за границей внешнего shadow-root**». План автотестов явно требует «монтировать карточку... внутри внешнего `attachShadow` и прокручиваемого ancestor» и отдельно уточняет, что «browser smoke обязан отдельно покрыть реальный scroll через внешний `attachShadow`». | Тело issue, раздел «Критерии приёмки» AC1 и AC2 (переформулированы), раздел «План автотестов» (пункты 1 и 3 переписаны). |
Обе находки закрыты в тексте, а не заявлением — переформулировка называет
конкретный механизм (обход composed-цепочки через `ShadowRoot.host`) и
конкретное требование к тесту (witness обязан пересекать границу стороннего
`attachShadow`), а не общие слова о «доработке».
Проверка на новое противоречие (по образцу самого H1, где «Контракт»
противоречил «Рискам»): в новой редакции «Контракт поведения» п.2 и «Риски»
п.2 согласованы — контракт обещает полный обход ancestor-chain, риск честно
называет цену этого обещания (пропуск ancestor/remount), не отрицая при этом
работоспособность самого механизма для заявленного сценария. Противоречия
того же типа, что в H1, не возникло.
Технической стороной решение также опирается на прецедент, уже
существующий в кодовой базе (`getRootNode()`-обход теневых границ в
`src/hp-dialog.ts` и `src/hp-zigbee-topology-overlay.ts`, зафиксированный в
r1) — то есть команда предлагает не новый непроверенный паттерн, а
переиспользование уже работающего в проекте приёма. Код не менялся, повторно
не читан в этом раунде — доверие к этому факту наследуется из r1, не
проверяется заново (см. ниже).
## Унаследовано из r1
Без повторной проверки в этом раунде — код между раундами не менялся,
а перечисленные разделы ТЗ дельтой r1→r2 не задеты:
- Наличие обязательных разделов §7.1 (сценарий, до/после, проблема,
скоуп/не-скоуп, UX, модель данных, i18n, план автотестов, риски, откат,
release-артефакты) — документ `SPEC-REVIEW-613-r1.md`, раздел «Что проверено
и корректно».
- Соответствие двух утверждений ТЗ о текущем поведении кода (`_saveZoom()` на
каждый `pointermove` в обоих pinch-путях; индекс не инвалидируется на
scroll) — подтверждено чтением `src/device-hit-owner.ts` и
`src/houseplan-card.ts` на дереве `1241b9499c7b` (`SPEC-REVIEW-613-r1.md`,
раздел «Как проверялось»). Дерево не менялось (см. «Как проверялось» выше).
- Реализуемость контракта арбитража #564 (`DevicePointerOwnerLatch`) на
существующем механизме без структурных изменений — `SPEC-REVIEW-613-r1.md`.
- Существование обоих smoke-файлов (`demo/smoke_device_hit_capsules.mjs`,
`demo/smoke_editor_gestures.mjs`) и `scripts/mutation-registry.mjs` —
`SPEC-REVIEW-613-r1.md`.
- Выбор полного трека (а не лёгкого) — обоснование в аналитике автора и
подтверждение r1 не изменились: дельта не меняет число затронутых
поверхностей.
- Отсутствие продуктовых вопросов к владельцу — дельта r1→r2 полностью
техническая (механизм инвалидации, формулировка AC), продуктовая рамка
(сценарий, до/после, UX) не тронута.
- AC3, AC4, AC5, AC6 как формулировки — не менялись текстом; их согласованность
с AC1/AC2 проверена в этом раунде заново (см. «Скоуп ревью» и «Находки»),
поскольку AC6 ссылается на AC1/AC2 напрямую («мутант... падает на AC1/AC2»).
Документ и материал первоисточника: `docs/reviews/SPEC-REVIEW-613-r1.md`,
блок «Материал раунда» — дерево `9a4518ab2014ca6190468cd459762dedbcf1ff0f`,
ветка `issue/613-scroll-hit-pinch-persist` на `1241b9499c7b`, вердикт `yellow`,
High 1.
## Находки
Нет находок High или Medium. Переформулировка точечно закрывает обе находки
r1 корректным техническим решением, не вносит новых противоречий и не
затрагивает разделы, не относящиеся к H1/M1.
**Low (снята с записью, не блокирует).** Формулировка AC2 «включая ancestor
за границей внешнего shadow-root» синтаксически двусмысленна при первом
прочтении отдельно от AC1 (можно прочесть как «ancestor, который находится
снаружи внешнего shadow-root», а не как задумано — «ancestor на границе или
за пределами внешнего shadow-root в сторону ещё более глубокой вложенности»).
Двусмысленность снимается контекстом: AC1 уже фиксирует ровно один уровень
вложенности («внешний `attachShadow`»), а «Контракт поведения» п.2 и
«Принятые технические предположения» однозначно описывают полный обход
цепочки произвольной глубины. Разработчик, реализующий AC2 в паре с AC1 и
контрактом, не может ошибиться в объёме требования. Снимаю без правки текста
ТЗ — стоимость уточнения формулировки выше цены двусмысленности, которая
разрешается соседними разделами того же документа.
## Что проверено и корректно
- Обе находки r1 (H1, M1) закрыты по существу, не косметически: изменившийся
раздел «Принятые технические предположения» выбирает именно тот вариант
решения, который r1 назвал технически реализуемым (обход composed-цепочки
через `ShadowRoot.host`), а не переформулировку прежнего нерабочего
предположения другими словами.
- Новая формулировка AC1/AC2 требует witness, пересекающий границу стороннего
`attachShadow`, и явно не позволяет заменить его light-DOM вариантом
(«Дополнительный light-DOM вариант допустим, но не заменяет этот witness») —
ровно то, что M1 требовал явно записать.
- «Контракт поведения» и «Риски» в новой редакции не противоречат друг другу
(проверено по образцу конфликта, найденного в H1) — контракт обещает то, что
риски признают дорогим, но не невозможным.
- План автотестов согласован с изменёнными AC: явно называет, какой смок
доказывает cross-shadow сценарий, а что допустимо доказывать управляемым
contract-фикстуром (`visualViewport`/reconnect) — граница между
«обязательный browser witness» и «допустимый unit-fixture» проведена по
тому же принципу, что и в AC1.
- Дельта не тронула ни одного из шести AC по существу их идентификаторов и
число (AC1…AC6 те же), не расширила и не сузила скоуп/не-скоуп, не добавила
и не убрала пункты модели данных, миграции, отката, release-артефактов —
никакого «попутного» изменения задачи вместе с точечным фиксом не произошло.
- Код между раундами не менялся (`git diff` по `src/`, `demo/`, `scripts/`
пуст) — утверждения ТЗ о текущем поведении кода, подтверждённые в r1,
остаются в силе без повторной проверки.
## Чего не проверял
- Не читал повторно `src/device-hit-owner.ts` и `src/houseplan-card.ts` —
код не менялся, доверие к прошлому чтению унаследовано из r1 (см.
«Унаследовано из r1»).
- Не проверял реальную DOM-структуру Home Assistant Lovelace вживую в этом
раунде — вывод об архитектуре HA frontend не менялся между раундами и не
является предметом дельты; проверялось в r1.
- Не гонял `tsc`/`test`/`build`/смоки/инварианты — стадия `spec`, кода для
прогона нет (код между раундами не менялся, что подтверждено `git diff`,
а не просто предположено).
- Не оценивал производительность или корректность конкретной реализации
lifecycle-helper (сколько слушателей, какая структура данных) — это решение
за автором на этапе реализации, явно помечено в ТЗ как «можно свободно
изменить на ревью», и не предмет спецификационного ревью.
- Не проверял issues #563/#578/#582 повторно — вне скоупа, не задеты дельтой.
## Вывод
Обе находки r1 (H1 High, M1 Medium-в-скоупе) закрыты точечно и по существу:
изменившиеся 6 из 22 разделов ТЗ выбирают технически реализуемый механизм
(composed ancestor-chain вместо `ownerDocument`-capture) и требуют witness,
пересекающий границу стороннего `attachShadow`, — ровно то, что r1 назвал
условием возврата. Новых High/Medium не найдено, одна Low-находка (двусмысленность
формулировки AC2) снята с записью как не создающая риска на практике. ТЗ
готово к статусу «Готово к разработке».
Вердикт: зелёный.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/613-scroll-hit-pinch-persist`, коммит `d36b7f80c320` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `58cda05f89d57c19a721dd12800ec32654231dcb`
```
git log --all --format='%H %T' | grep 58cda05f89d5
```
- Тело issue: `98d86d7e8cd4c13b732452c7b4c391e31cf302d3636fbe1ef0c8a7aeb5dac9e3`
- Вердикт конвейера: `green` · High 0