Files
houseplan-card/docs/reviews/SPEC-REVIEW-179-r1.md
T
2026-08-19 19:05:01 +00:00

272 lines
23 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.
# Ревью ТЗ — issue #179, цикл r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/179
- **ТЗ:** `docs/specs/179-device-icons-redesign.md` (коммит `517a710`, ветка
`issue/179-device-icons-redesign`)
- **Трек:** обычный (аналитика явно исключила `small`/`trivial`)
- **Вердикт:** жёлтый · цикл r1/4 · High: 0 · Medium: 5 (все в скоупе задачи) · Low: 2
## Скоуп ревью
Проверялось только ТЗ как артефакт этапа `S4-spec-review`: обязательные разделы
§7.1 PROCESS.md, однозначность и доказуемость AC1…AC14, соответствие
`docs/SCOPE.md` (J1/J2/J7), `docs/USER-GUIDE.ru.md`, `docs/CONFIG-COMPATIBILITY.md`,
`docs/TOUCH-SUPPORT.md`, а также фактическое соответствие ТЗ нормативному
источнику — архиву дизайнера, приложенному к issue #179. Продуктовый код не
существует (issue не дошёл до `S5-ready`), поэтому код не проверялся.
## Как проверялось
1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком.
2. Прочитано тело issue #179 и все три комментария (`gh issue view 179 --json ...`).
3. Прочитан файл ТЗ целиком (534 строки).
4. **Архив дизайнера скачан и распакован** (`curl` по ссылке из тела issue,
`sha256sum` совпал с указанным в ТЗ `63670C73...`), чтобы проверить не только
внутреннюю согласованность ТЗ, но и его фактическое соответствие
нормативному источнику — это единственный способ отличить решение владельца
от догадки, выданной за факт:
- `SPECIFICATION.md`, `ACTIVE_ANIMATION_SPEC.md`, `DEVELOPER_HANDOFF.md`,
`README.md`, `PACKAGE_ANNOTATION.txt`, `manifest.json` — построчно;
- геометрия и цвета — вычислены из реальных SVG (`viewBox`, `<rect>`,
`<path>` координаты circle-shell, `fill`/`stroke`, `feColorMatrix`);
- тайминги анимаций — вычислены из `@keyframes`/`animation` в
`Animated/Light/*.svg` (текстовые документы архива тайминги короткого
события и alert не дают, только качественное описание).
5. Сверено с текущей реализацией: `src/device-pulse.ts`,
`src/device-presentation.ts`, `src/styles.ts`, `src/types.ts`,
`src/houseplan-card.ts` — чтобы отличить утверждения ТЗ о «текущем
поведении» (раздел «До реализации») от факта, и проверить, действительно ли
новая раскладка цветов континуальной пульсации — новшество.
6. Прочитаны релевантные фрагменты `docs/USER-GUIDE.ru.md` (терминология
состояний, приоритет статусов, пульсация, «Всегда статичный значок», LQI
комнаты) и `docs/CONFIG-COMPATIBILITY.md`, `docs/TOUCH-SUPPORT.md` целиком.
7. Код не запускался, гейты не прогонялись — на этапе ревью ТЗ кода нет,
раздел «Гейты» PROCESS.md §8 к этому циклу неприменим.
## Находки
### M1 — цвет внешней тени искажён относительно нормативного источника
**Файл:** `docs/specs/179-device-icons-redesign.md:122-123`
ТЗ утверждает для light-shell на референсе 56 px: `0 1px 2px rgb(0 0 0 / 12%)`
и `0 4px 8px -1.07px rgb(0 0 0 / 18%)` — чистый чёрный.
Нормативный источник (`SPECIFICATION.md`, таблица §8) даёт другой цвет:
`0 1px 2px 0 rgb(37 40 45 / 12%)` и `0 4px 8px -1.07px rgb(37 40 45 / 18%)`.
Это подтверждается самим SVG: `feColorMatrix` в `Light/Icon Default.svg` задаёт
`0.145098 / 0.156863 / 0.176471` — это ровно `37/255, 40/255, 45/255`, то есть
тёплый тёмно-серый `#25282D`, а не `#000000`.
По порядку приоритета самого ТЗ (§3: решения владельца → текстовые документы
архива → SVG-примеры → текущая реализация) текстовый документ и SVG совпадают
между собой и расходятся с ТЗ; решения владельца по этому пункту нет. Это не
разночтение источников, а фактическая ошибка транскрибирования в самом ТЗ.
**Почему это важно:** AC1 обещает golden-эталон 32/56/96 px по геометрии и
токенам пакета. Реализация по тексту ТЗ буквально даст на глаз более резкую,
холодную тень, чем в дизайне, и это will пройти собственный (неверный) golden,
проверяющий не то, что задумано.
**Требуется:** заменить `rgb(0 0 0 / …)` на `rgb(37 40 45 / …)` в §7.1 (обе
тени, 56 px референс; проверить также, что при масштабировании на 32/96 px в
реализации используется тот же цвет).
### M2 — цвет continuous-пульсации для reason `presence` не определён
**Файл:** `docs/specs/179-device-icons-redesign.md:222-230` (§10.1), `:129-144` (§7.2)
Пакет содержит **три** статических и три анимированных варианта Continuous
Working: `Default` (обводка `#0C82F0`), `Yellow` (`#F0A00C`), `Green`
(`#1DC21D`). `SPECIFICATION.md` §4 перечисляет источники непрерывной работы:
«свет, вентилятор, климат, уборка, **присутствие**» — но ни один текстовый
документ архива (`SPECIFICATION.md`, `ACTIVE_ANIMATION_SPEC.md`,
`DEVELOPER_HANDOFF.md`) не говорит, какому из трёх цветов соответствует какая
причина. Green нигде не привязан к семантике — ни в архиве, ни в ТЗ.
Это не абстрактная тонкость: в текущей реализации `resolveDevicePulse`
(`src/device-pulse.ts:93-103`) уже различает три причины continuous-пульсации
— `presence`, `transition`, `running` — но все три сегодня рисуются **одним**
переданным цветом, без различия по причине. Пакет впервые вводит три разных
цветовых варианта того же мотива, а ТЗ §7.2/§10.1 говорит только «цвет
соответствует resolved ordinary activity», не решая: (а) вводится ли новое
различение по `reason` (presence → зелёный, как логично предположить по тексту
пакета) или (b) `Green`-ассет в этой итерации не используется вовсе.
AC8 требует, чтобы «цвета соответствовали §10» — при текущем тексте §10 это
непроверяемо для presence-пульсации, потому что нормативного значения нет ни в
одном документе.
**Требуется:** явное решение (не владельца — это техническое/дизайн-решение,
которое ТЗ вправе принять само, см. PROCESS.md §7.1 "всё, чего пользователь не
наблюдает, агенты решают сами"; здесь пользователь наблюдает результат, но
выбор между двумя понятными вариантами не требует владельца) — либо
зафиксировать маппинг `presence → Green`, либо явно исключить `Green` из
поставки с пометкой «принято предположительно» в §20.
### M3 — easing для Short и Alert не зафиксирован
**Файл:** `docs/specs/179-device-icons-redesign.md:232-246` (§10.2, §10.3)
§10.1 (Continuous) корректно и проверяемо квотирует
`cubic-bezier(.45,.05,.55,.95)` — совпадает с `ACTIVE_ANIMATION_SPEC.md` и с
`working-ring`/`working-core-blue` в `Animated/Light/Continuous Working Default
Animated.svg`. §10.2 (Short) и §10.3 (Alert) дают duration, delay, scale,
stroke, цикл — но **не** easing. В реальных SVG (`Animated/Light/Short
Activity Animated.svg`, `Animated/Light/Alert Animated.svg`) обе анимации
используют `cubic-bezier(.22,.61,.36,1)` — другую кривую, чем Continuous.
`ACTIVE_ANIMATION_SPEC.md` этот параметр текстом тоже не называет, только
качественно («цикл должен быть плавным»), то есть источник факта — только
SVG-пример (третий приоритет по ТЗ), и раз он существует и однозначен, ТЗ
должен был его перенести.
AC8 обещает «timings ... и colors соответствуют §10» — без easing реализация
по умолчанию получит линейное или произвольное ускорение, что даст другое
ощущение движения и не пройдёт визуальную приёмку по духу задачи ("читаемое
глазами" состояние).
**Требуется:** добавить `easing: cubic-bezier(.22,.61,.36,1)` в §10.2 и §10.3.
### M4 — стоимость и выбор backdrop-blur для Dark не решены
**Файл:** `docs/specs/179-device-icons-redesign.md:125-127` (§7.1), §17 (риски), AC13
`manifest.json` прямо объявляет `"backdropBlurPx": 20` для темы Dark, и
`Dark/Icon Default.svg` реально содержит `foreignObject` с
`backdrop-filter:blur(20px)` — а не просто SVG-тень. Пакет отдельно поставляет
`Dark/Icon Default No Blur.svg`, который побайтно совпадает с `Icon
Default.svg` за вычетом именно этого слоя (проверено diff'ом), и, в отличие от
Light-алиаса, **не** помечен в `manifest.json` как `deprecated`/`aliasOf` — то
есть это не забытый дубликат, а отдельный, специально сохранённый fallback.
ТЗ §7.1 ограничивается «Dark использует неизменённую dark-ревизию пакета…
эталонные эффекты» — не решая, войдёт ли `backdrop-filter: blur(20px)` **на
каждый маркер** в продакшен, или используется No-Blur вариант. Это не мелочь:
`backdrop-filter` — дорогая по композитингу операция; `docs/SCOPE.md` называет
20–200 устройств на план и киоск-планшет как основную View-поверхность для двух
из трёх персон (`docs/TOUCH-SUPPORT.md`: View/kiosk — блокирующие). В `src/`
сегодня `backdrop-filter` не применяется ни к одному маркеру (только к
editor-tray, `src/editor-secondary.styles.ts`), так что это действительно новая
для маркеров нагрузка, а не продолжение существующей. AC13 («нет per-frame JS,
ResizeObserver…») не упоминает composite-cost blur-слоёв, и §17 «Риски» этот
риск не называет вовсе.
**Требуется:** явное решение — либо применять blur (тогда назвать это в §17 и
покрыть производительность в AC13/perf-профиле), либо взять готовый No-Blur
Dark-ассет как основной рендер продакшена (быстрее, безопаснее для 200
устройств), с записью решения и причины.
### M5 — не назван обязательный тег `Touch editor:` из TOUCH-SUPPORT.md
**Файл:** `docs/specs/179-device-icons-redesign.md` (документ целиком)
`docs/TOUCH-SUPPORT.md`, раздел «Documentation rule»: «New editor feature
specifications and code reviews must state one of: `Touch editor: supported`;
`Touch editor: best effort / intentionally degraded`; `Touch editor: not
exposed`.» ТЗ меняет интерактивный маркер (shell, hit area, focus/keyboard) на
поверхности **Device editor** (§11: «Интерактивные маркеры в View/kiosk и
Device editor получают `tabindex="0"`…»), то есть подпадает под правило, но
нигде в документе буквально не заявляет ни один из трёх канонических тегов.
Канонический документ подсистемы даёт готовый ответ («Device editor: Best
effort» — из таблицы контракта), ТЗ просто не переносит его явным образом.
**Требуется:** добавить одну строку вида `Touch editor: best effort /
intentionally degraded` (или `supported`, если автор считает keyboard/hit-area
изменения полностью бесплатными для touch — решение автора, не владельца) в
раздел §11 или §17.
### L1 — Light-only combo-эталоны (`Selected + Hover/Working/Green/Alert`,
`Focus + Alert`) не отражены в golden-матрице
`manifest.json` для Light содержит пять составных QA-эталонов
(`Selected + Hover.svg` и т.д.), у Dark таких файлов нет вовсе (не только не
хватает файлов — узлы Figma для них не заведены). §16.4 (golden matrix)
перечисляет одиночные состояния, но не комбинации Selected/Focus с semantic
state отдельно для Dark. Поскольку приоритет слоёв (§7.3) декларативный и
реализуется одним рендерером, а не набором ассетов, это не блокирует
разработку — но стоит явно зафиксировать в golden-матрице, что комбинации
проверяются для обеих тем логически, а не по образцу (в Dark образца нет).
Снимается автором с записью либо на одну строку в §16.4.
### L2 — избыточная оговорка «unavailable hover запрещён» для `static_icon`
§7.3 говорит про `static_icon`: «на интерактивной поверхности он сохраняет
разрешённые hover/focus/click; unavailable hover запрещён». Действующее
поведение `static_icon`, зафиксированное в `docs/USER-GUIDE.ru.md:811-816`,
таково, что этот режим **вообще не входит** в ветку unavailable («не
показывает… недоступность»). Формулировка ТЗ не противоречит этому, но звучит
как отдельное правило для случая, который по документированному контракту не
может произойти. Не блокирует; можно оставить как защитную формулировку или
убрать как мёртвый кейс — на решение автора.
## Проверено и признано корректным
- **SHA-256 архива** (`63670C73E25D1E59DDAF1BE236F3D7F2FAC827B9B5D6DD4B77125EA9BC012025`)
совпадает с реально скачанным файлом по ссылке из issue.
- **Геометрия shell/core**: круговой shell — путь-круг с центром `(63.5, 55.5)`
и радиусом `50.75` (диаметр `101.5`), core — `<rect>` `80×80` с тем же
центром. `101.5/127 ≈ 0.799`, `80/101.5 ≈ 0.788` — оба числа в ТЗ (§7.1)
подтверждены вычислением по реальному SVG, не переписаны на глаз.
- **Цвета состояний** (§7.2): hover/focus `#0C82F0`, active/working/unlocked
`#F0A00C`, alert `#F0410C`, unavailable core `#B5BAC1`, high LQI `#1DC21D`,
lock `black` — все сверены построчным `grep` по `fill`/`stroke` в
соответствующих Light SVG и совпадают.
- **Continuous/Short/Alert тайминги** (кроме easing Short/Alert — см. M3):
3.6 с / scale 1→1.5 / opacity .55→0 для Continuous и 1.1 с × 3 с задержками
0/1.1/2.2 с для Short — оба подтверждены и текстовым документом архива, и
реальными `@keyframes`.
- **Обнаруженная и верно разрешённая ошибка пакета**: `Light/Zigbee LQI
Low.svg` действительно закрашен `#F0A00C` (тот же цвет, что Mid), а не
красным. Утверждение ТЗ §9/§20.2 о том, что это ошибка экспорта и что
production-эталон — red по текстовой спецификации и решению владельца,
подтверждено — это пример правильно задокументированного разрешения
конфликта источников, а не догадки.
- **LQI-границы 40/180** согласуются с уже существующей документированной
границей комнатного LQI-градиента (`docs/USER-GUIDE.ru.md:873`: «градиент от
красного (≤40) до зелёного (≥180)») — решение владельца не произвольно, оно
продолжает существующий продуктовый факт.
- **Секьюрность**: §11 явно исключает новый путь обхода подтверждения для
замков/клапанов через клавиатуру — Enter/Space вызывают тот же существующий
click handler. Инвариант `docs/SCOPE.md` («никогда не по тапу без
подтверждения») не нарушается ни в одном пункте ТЗ.
- **Совместимость** (§12): поведение "Open → Save не материализует
`ripple_size`/`ripple_color` при смене UI-default 3→1.5" явно и правильно
учитывает существующий паттерн записи (`d.rippleSize !== 3 ? … : null` в
`src/houseplan-card.ts:18448`) и предотвращает тихий сброс уже явно
сохранённых значений.
- **Обязательные разделы §7.1 PROCESS.md** — все присутствуют: сценарий,
что видит пользователь до/после, проблема, скоуп/не-скоуп, контракт
поведения, UX/доступность, данные/миграция, i18n, AC1…AC14 с доказательством,
план автотестов, риски, откат, release-артефакты.
- **Трассируемость**: `docs/specs/README.md` обновлён в том же коммите с
двусторонней ссылкой issue↔ТЗ; коммит `517a710` несёт корректные трейлеры
`Issue: #179` и `User-Visible: no`.
- **Открытых продуктовых вопросов владельцу нет** — все решения, требующие
продуктового выбора (unavailable hover, приоритет pulse-настроек, границы
LQI, клавиатура), приняты владельцем в комментарии аналитики и перенесены в
§4 ТЗ без искажений.
## Чего не проверял
- **Figma-фреймы** (`99:1290`, `104:1539`) напрямую не открывались — ревью
ограничено содержимым архива, доступного по ссылке из issue; это тот же
источник, которым пользовался автор ТЗ.
- **`Index.html`/PNG-визуализации архива** не открывались в браузере —
ревью на этапе ТЗ смотрит на текст и код, не на артефакт для дизайнеров;
визуальная проверка релевантна golden-этапу code review.
- **Полная построчная сверка размеров 32 px и 96 px** (только 56 px эталон
проверен вычислением; 32/96 в ТЗ не квотируются отдельно, они делегированы
таблице `SPECIFICATION.md` §8 без искажения).
- **Производительность** не измерялась (кода нет); M4 — это находка на уровне
решения, а не измеренный факт деградации.
- Гейты `typecheck`/`test`/`build`/smoke/golden не запускались — на этапе
ревью ТЗ продуктового кода не существует, раздел неприменим.
## Итог
Ни одна находка не блокирует по существу — все пять Medium устранимы точечными
правками текста ТЗ (это ревью самого текста, а не архитектурного решения), но
без них AC1, AC8 и AC13 не выполняют собственное обещание доказуемости
(«соответствует §10», «соответствует пакету», «нет неизвестных perf-рисков»).
Вердикт — жёлтый, возврат автору в рамках текущего issue.