Files
houseplan-card/docs/reviews/SPEC-REVIEW-534-r1.md
T
2026-09-11 18:53:32 +00:00

20 KiB
Raw Blame History

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/replace device-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 (любое из трёх, решает автор):

  1. исправить допущение и сохранить пер-элементную защиту внутри пространства — например, keyed(space.id, () => repeat(devs, (d) => d.id, …)): смена space.id по-прежнему выбрасывает поддерево целиком без дорогого сравнения с 200 чужими ключами (устраняет весь измеренный регресс), а repeat внутри защищает от переупорядочивания в пределах одного пространства, как и сегодня; либо
  2. показать конкретным измерением/рассуждением, что состав devs в реальных сценариях (не только в фикстуре бенчмарка) не меняется без смены space.id так, чтобы порядок сдвигался — и явно принять остаточный риск в разделе «Риски», а не в «Принятые предположения» как решённый факт; либо
  3. добавить 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.
  • Дерево материала: e87f2fb0f5a99184cd9795a5e59155b96b3ac2f6
    git 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 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: e87f2fb0f5a99184cd9795a5e59155b96b3ac2f6
    git log --all --format='%H %T' | grep e87f2fb0f5a9
    
  • Тело issue: 3ba57688eafe3310c9e3d0157758ea519c3b89d980958e504d2c2b54b3a43cb3
  • Вердикт конвейера: yellow · High 0