20 KiB
SPEC-REVIEW-534-r1
Скоуп
Issue #534 (bug, P1, полный трек — критерий §5 «нет влияния на
производительность» явно назван нарушенным в самой ТЗ). ТЗ живёт в теле
issue под заголовком ## ТЗ (решение владельца #517). Issue открыт, метка
S4-spec-review, заход r1, бюджет циклов 0/4 не тронут.
Предмет: линейка v1.74.0 добавила 7 длинных задач P95 на профилях
large-house и plan-snap — регресс внесён #525 (перевод списка маркеров
устройств на repeat(devs, (d) => d.id, …), чтобы Lit не переиспользовал
узел позиционно и не проигрывал переход на неслучившемся событии). На 200
маркерах repeat строит и обходит карту ключей на каждый рендер, а при
смене пространства ключи не пересекаются вовсе — диф оплачивается напрасно.
Решение ТЗ: обернуть слой маркеров в keyed(space.id, …)
(lit/directives/keyed.js) вокруг обычного map(), оставить проёмы на
repeat (там элементов на порядок меньше).
Как проверялось
Прочитаны docs/SCOPE.md, PROCESS.md (§1–§10, включая §7.1, §2.4, §2.10,
§5, §8), тело issue #534 целиком и единственный комментарий (аналитика
Codex, S2, с бисектом и измерением на пробе). Дополнительно, раз ТЗ
опирается на конкретные строки, числа и поведение существующих механизмов,
прочитан репозиторий на материале ревью (SHA 41ae5e99, см. блок якорей):
src/houseplan-card.ts:11787(repeat(devs, (d) => d.id, …), слой маркеров) и:12996(repeat(items, (o) => o.id, …), слой проёмов) — оба подтверждены чтением, ссылки в ТЗ точны;node_modules/lit/directives/keyed.js— директива существует в составе уже установленной зависимостиlit, ссылка К2 на неё не голословна;scripts/mutation-gate.mjs:7440(openings-rendered-without-keys) и:7446(device-markers-rendered-without-keys) — оба существующих мутанта нацелены ровно на строки, которые правка меняет; find/replacedevice-markers-rendered-without-keysперестанет применяться после правки (текст${repeat(devs, …)}исчезнет) — ТЗ (AC5) корректно предвидит необходимость его переанкеровки;demo/smoke_space_switch_transitions.mjs— прочитан целиком. Подтверждено: AC1/AC2 действительно можно закрыть существующим смоком без правок — проверкаnoMarkerNodeReusedForAnotherDevice(строки 124–130) судит по идентичности узла (entry.node.isConnected && entry.node.dataset.id !== entry.id), а не по побочному признаку, как и требует AC2;src/styles/plan.styles.ts:531–542— комментарий-ловушка #525 существует, называет.device-shell-frame(box-shadow) как причину для маркеров; ТЗ в разделе «Затронутые файлы» верно указывает, что этот комментарий нужно дополнить (К3);src/styles/devices.styles.ts:196-217— проверена природа переходов на маркере (детали см. в находке ниже);demo/fixtures/large-house.mjs:10-11—DEVICE_COUNT = 200,OPENING_COUNT = 100(на 3 этажа, т.е. ~33 на пространство) — цифры ТЗ о 200 маркерах точны; слово «единицы» про проёмы (К3) — преувеличение (реально несколько десятков на этаж), но не влияет ни на один AC;docs/CHANGELOG.md/docs/CHANGELOG.ru.md— записи #524/#525 в разделе «Unreleased»/«Не выпущено» существуют, формат черновика (AC7) совпадает со стилем файла;test/device-marker-polish-contract.test.mjs,test/space-order.test.mjs,test/device-face.test.mjs— ни один не проверяет позиционную стабильность маркера при изменении состава списка внутри одного пространства (относится к находке ниже).
Гейты не гонялись: на этапе ревью ТЗ продуктовый код не менялся, дешёвые
гейты (typecheck/test/build) к этому этапу неприменимы (§2.4, §8).
Продуктовая рамка
Сценарий и «что человек увидит» сформулированы одной фразой без терминов
реализации, как требует §7.1: «каждое переключение дороже примерно на
80 мс главного потока… карточка перестаёт отвечать на ввод чуть дольше,
чем должна» → «переключение возвращается к цене, которая была до #525».
Персона — домочадцы/гости на большом плане, docs/SCOPE.md называет View
mode продуктом для этих персон; регресс блокирует именно это. Полный трек
обоснован явно названным нарушенным критерием §5 («нет влияния на
производительность»), а не молчаливым выбором — соответствует требованию
2026-08-27. i18n/миграция/бэкенд и touch закрыты явным «ничего» /
«тот же путь» — ТЗ учло Low-находку из SPEC-REVIEW-525-r1 (там эти пункты
пришлось поднимать отдельно), здесь они на месте с самого начала.
Разбор AC
- AC1 — «остаётся зелёным без правок»: подтверждено чтением, существующий
смок уже проверяет то, что требуется, и правка К2/К3 его не задевает
структурно (тест не завязан на
repeatизнутри, только на видимое поведение DOM/CSS). - AC2 — свидетель по идентичности узла уже существует (см. «Как проверялось»), формулировка ТЗ точно описывает то, что тест делает.
- AC3 — численный ориентир (910 мс) подтверждён измерением автора на
main+ патч (903,7 мс), не выдуман. - AC4 — внешний гейт (Full Performance), критерий однозначен (девять профилей, включая оба проблемных).
- AC5 — корректно предвидит необходимость правки мутанта (подтверждено выше — старый find/replace перестанет матчиться).
- AC6/AC7 — golden и ревью кода, стандартные, без двусмысленности.
Двусмысленности, которая заставила бы гадать «что считается прохождением», в текстах AC не нашёл.
Находки
Medium — ключевое техническое допущение К2/«Принято предположительно» №2 неверно, и это открывает непроверенный регресс той же природы, что чинит вся задача
Утверждение ТЗ: «Внутри одного пространства позиционное переиспользование маркеров безопасно: после #524 на маркере не осталось переходов, которые могла бы запустить смена значений.»
Проверено чтением и опровергнуто: src/styles/devices.styles.ts:213
несёт .device-shell-frame { … transition: border-color .15s, opacity .2s; }
— оба перехода живы. #524 убрал только переход box-shadow (см. комментарий
на той же строке: «no box-shadow here: cqw-sized, restarts on every
container resize (#524)»), а не «переходы» вообще — допущение спутало
конкретный фикс с общим случаем. --device-shell-stroke (цвет рамки)
переопределяется по состоянию устройства (devices.styles.ts:296,312,322,421
— alert-цвета) — то есть у двух РАЗНЫХ устройств в одном списке этот цвет
типично отличается.
Почему это ломает контракт К2. К2 предписывает конкретную реализацию:
keyed(space.id, …) вокруг обычного map() для слоя маркеров — то есть
внутри одного пространства элементы списка теряют пер-элементный ключ,
который сейчас даёт repeat(devs, (d) => d.id, …). Если состав devs
меняется без смены space.id — а он меняется: showGhosts (houseplan- card.ts:11307, this._mode === 'devices' && this._showAll) переключает
видимость части устройств в редакторе устройств, а само появление нового
устройства в пространстве от лайв-синка конфигурации (J6, «new-device
flag») тоже меняет состав devs без переключения пространства — Lit
переиспользует DOM-узлы по позиции, не по устройству. Маркер, стоявший
на позиции N, может получить данные другого устройства с другим
--device-shell-stroke/opacity, и уже живой transition: border-color .15s, opacity .2s эти узла честно проиграет — ровно тот класс дефекта
(«анимация события, которого не было»), который и #525, и эта самая задача
называют главным риском (Риск 1 в ТЗ), только для другого триггера: не
переключение пространства, а изменение состава списка внутри пространства.
Почему это не поймано ни одним AC. demo/smoke_space_switch_transitions.mjs
(AC1/AC2) тестирует только переключение между двумя пространствами; ни один
существующий тест (test/device-marker-polish-contract.test.mjs,
test/space-order.test.mjs, test/device-face.test.mjs) не проверяет
переход, вызванный изменением состава списка маркеров внутри одного
пространства. Риски ТЗ (1–3) эту ситуацию не называют.
Почему это в скоупе. К2 — это ровно тот код, который правит задача;
регрессия, если она проявится, будет внесена этим же коммитом, а не
существует независимо от него сегодня (сейчас защиту даёт per-item ключ
repeat, который К2 явно убирает).
Что нужно для DoR (любое из трёх, решает автор):
- исправить допущение и сохранить пер-элементную защиту внутри
пространства — например,
keyed(space.id, () => repeat(devs, (d) => d.id, …)): сменаspace.idпо-прежнему выбрасывает поддерево целиком без дорогого сравнения с 200 чужими ключами (устраняет весь измеренный регресс), аrepeatвнутри защищает от переупорядочивания в пределах одного пространства, как и сегодня; либо - показать конкретным измерением/рассуждением, что состав
devsв реальных сценариях (не только в фикстуре бенчмарка) не меняется без сменыspace.idтак, чтобы порядок сдвигался — и явно принять остаточный риск в разделе «Риски», а не в «Принятые предположения» как решённый факт; либо - добавить AC и свидетеля на сценарий «состав списка маркеров меняется внутри пространства» по образцу уже существующего свидетеля #525/#528.
Решение ревьюера: блокирует переход в S5-ready без правки одним из
трёх способов выше — Medium в скоупе задачи, чинится в этом же issue
(§2.4, §2.7 #202), отдельный issue не заводится.
Low — «их единицы» (К3) неточно описывает число проёмов
К3 обосновывает, почему проёмы остаются на repeat: «их единицы, диф ничего
не стоит». По фикстуре large-house (DEVICE_COUNT=200,
OPENING_COUNT=100 на 3 этажа) на пространство приходится порядка
30 проёмов — не «единицы», хотя и на порядок меньше 200 маркеров, так что
общий вывод (репит на проёмах дешевле) не меняется и ни один AC от этого не
зависит.
Решение ревьюера: не блокирует, снимается этой записью; можно поправить формулировку заодно с остальной правкой К2, отдельного цикла не открываю.
Что проверено и корректно
- Обязательные продуктовые разделы §7.1 (сценарий, что человек увидит до и после) присутствуют и однозначны.
- Полный трек обоснован явно названным нарушенным критерием §5.
- i18n, миграция/бэкенд, touch — явные «нет»/«тот же путь», а не молчание (что в #525 пришлось поднимать Low-находкой — здесь уже учтено).
- AC1–AC7 пронумерованы, у каждого указан способ доказательства; численные пороги AC3 (910 мс) и AC4 (девять профилей) проверяемы и не выдуманы — число AC3 сверено с измерением автора на пробе.
- AC5 корректно предвидит поломку существующего мутанта новой формой кода и требует его переанкеровки — не тихое исчезновение защиты.
- Технические ссылки (номера строк, наличие директивы
keyedв зависимостях, наличие мутантов, содержимое свидетеля) подтверждены чтением репозитория, ни одна не оказалась пустой. - Откат — один revert, без миграции данных, корректно.
- Риски 1 и 3 предметны и привязаны к AC/гейтам, которые их ловят.
- Release-артефакты (changelog RU+EN,
User-Visible: yes) названы верно.
Чего не проверял
- Не запускал никакого кода и ни одного гейта — на этапе ревью ТЗ
продуктовый код не менялся, дешёвые гейты (
typecheck/test/build) неприменимы к этой стадии (§2.4, §8). - Не проверял golden-эталоны и реальные тайминги на Chromium/раннере — появятся только с реализацией; численные ориентиры ТЗ (903,7 мс, 910 мс) доверены измерению автора (внутренне согласованы с бисектом в комментарии) и будут окончательно подтверждены Full Performance по AC4.
- Не проверял частоту реальных сценариев
showGhosts/лайв-добавления устройства «в проде» количественно — риск в находке выше обоснован кодом (условие фильтра, наличие живых CSS-переходов, наличие state-зависимого цвета рамки), а не измерением частоты; для итогового решения (пункт 1–3 находки) этого достаточно, эмпирическая частота не меняет того, что сценарий кодом не исключён и не покрыт тестом.
Вердикт
Жёлтый. Один Medium в скоупе задачи (допущение К2 о безопасности внутрипространственного позиционного переиспользования маркеров неверно и открывает непроверенный регресс той же природы, которую чинит вся задача); High нет. Возврат автору на правку одним из трёх названных способов; одна Low-находка о формулировке снята этой записью.
Материал раунда
- Ветка:
dev, коммит41ae5e99868db27e3bfde9a40426c0a636c374bd. - Дерево материала:
e87f2fb0f5a99184cd9795a5e59155b96b3ac2f6git log --all --format='%H %T' | grep e87f2fb0f5a9 - Тело issue (нормализовано
gh issue view 534 --json body -q '.body'):sha256:98e12a0f7bec0c050802b1caee9c139db3bbc7b7c1169744fb1ccd567e35babd - Вердикт:
yellow· High 0 · Medium 1 (в скоупе)
Материал раунда
- Ветка:
dev, коммит41ae5e99868d— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
e87f2fb0f5a99184cd9795a5e59155b96b3ac2f6git log --all --format='%H %T' | grep e87f2fb0f5a9 - Тело issue:
3ba57688eafe3310c9e3d0157758ea519c3b89d980958e504d2c2b54b3a43cb3 - Вердикт конвейера:
yellow· High 0